diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..9591e75e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +ARG BUILD_VERSION=1.0.0.0 +WORKDIR /work +COPY ["src/Papercut.Service/Papercut.Service.csproj", "/work/Papercut.Service/"] +COPY ["src/Papercut.Common/Papercut.Common.csproj", "/work/Papercut.Common/"] +COPY ["src/Papercut.Core/Papercut.Core.csproj", "/work/Papercut.Core/"] +COPY ["src/Papercut.Infrastructure.IPComm/Papercut.Infrastructure.IPComm.csproj", "/work/Papercut.Infrastructure.IPComm/"] +COPY ["src/Papercut.Infrastructure.Smtp/Papercut.Infrastructure.Smtp.csproj", "/work/Papercut.Infrastructure.Smtp/"] +COPY ["src/Papercut.Message/Papercut.Message.csproj", "/work/Papercut.Message/"] +COPY ["src/Papercut.Rules/Papercut.Rules.csproj", "/work/Papercut.Rules/"] +RUN dotnet restore "/work/Papercut.Service/Papercut.Service.csproj" +COPY . . + +#RUN sed "s/\(Assembly\(Informational\|File\)Version(\d34[0-9]\+\.[0-9]\+\.[0-9]\+\.\)[0-9]\+/\1$BUILD_VERSION/" src/GlobalAssemblyInfo.cs + +WORKDIR "/work/src/Papercut.Service" + +RUN dotnet build "Papercut.Service.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Papercut.Service.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final + +ARG BUILD_VERSION +ARG BUILD_DATE +ARG VCS_REF + +LABEL org.opencontainers.image.title="Papercut SMTP Service" \ + org.opencontainers.image.description="Papercut SMTP Service is a 2-in-1 quick email viewer AND built-in SMTP server" \ + org.opencontainers.image.version=${BUILD_VERSION} \ + org.opencontainers.image.url="https://www.papercut-smtp.com/" \ + org.opencontainers.image.source="https://github.com/ChangemakerStudios/Papercut-SMTP" \ + org.opencontainers.image.created=${BUILD_DATE} \ + org.opencontainers.image.revision=${VCS_REF} \ + org.opencontainers.image.licenses="Apache License, Version 2.0" + +WORKDIR /app + +COPY --from=publish /app/publish . + +ENV ASPNETCORE_HTTP_PORTS=80 + +# HTTP +EXPOSE 80 + +# SMTP +EXPOSE 25 + +# optional -- should only be used locally: IPComm +# EXPOSE 37403 + +CMD ["dotnet", "Papercut.Service.dll"] diff --git a/Papercut.sln b/Papercut.sln index 5fb21731..4f709f21 100644 --- a/Papercut.sln +++ b/Papercut.sln @@ -34,6 +34,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Build", ".Build", "{8661B3 GitVersion.yml = GitVersion.yml build\ReleaseNotes.cake = build\ReleaseNotes.cake ReleaseNotes.md = ReleaseNotes.md + .dockerignore = .dockerignore + Dockerfile = Dockerfile + README.md = README.md + build-docker.sh = build-docker.sh + build-docker.ps1 = build-docker.ps1 EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Papercut.Infrastructure.Smtp", "src\Papercut.Infrastructure.Smtp\Papercut.Infrastructure.Smtp.csproj", "{873EC485-8E94-4877-8EA7-A7DFE7612E0A}" diff --git a/README.md b/README.md index 946f76e8..a0d4b63d 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ [![Build status](https://ci.appveyor.com/api/projects/status/bs2asxoafdwbkcxa?svg=true)](https://ci.appveyor.com/project/Jaben/papercut-smtp) ## The problem -If you ever send emails from an application or web site during development, you're familiar with the fear of an email being released into the wild. Are you positive none of the 'test' emails are addressed to colleagues or worse, customers? Of course, you can set up and maintain a test email server for development -- but that's a chore. Plus, the delay when you are waiting to view new test emails can radically slow your development cycle. +If you ever send emails from an application or website during development, you're familiar with the fear of an email being released into the wild. Are you positive none of the 'test' emails are addressed to colleagues or worse, customers? Of course, you can set up and maintain a test email server for development -- but that's a chore. Plus, the delay when waiting to view new test emails can radically slow your development cycle. ## Papercut SMTP to the rescue! -Papercut SMTP is a 2-in-1 quick email viewer AND built-in SMTP server (designed to receive messages only). Papercut SMTP doesn't enforce any restrictions how you prepare your email, but it allows you to view the whole email-chilada: body, html, headers, attachment down right down to the naughty raw encoded bits. Papercut can be configured to run on startup and sit quietly (minimized in the tray) only providing a notification when a new message has arrived. +Papercut SMTP is a 2-in-1 quick email viewer AND built-in SMTP server (designed to receive messages only). Papercut SMTP doesn't enforce any restrictions on how you prepare your email, but it allows you to view the whole email-chilada: body, HTML, headers, and attachment right down to the naughty raw encoded bits. Papercut can be configured to run on startup and sit quietly (minimized in the tray) only providing a notification when a new message has arrived. ## Download Now ### [Download the Papercut.Setup.exe installer](https://github.com/ChangemakerStudios/Papercut-SMTP/releases) ## Requirements -Papercut SMTP UI Requires the "WebView2" Microsoft shared system component be installed on your system. If you have any problems getting it running go to this site: +Papercut SMTP UI Requires the "WebView2" Microsoft shared system component to be installed on your system. If you have any problems getting it running go to this site: [WebView2 Download](https://developer.microsoft.com/en-us/microsoft-edge/webview2) and install it. ## Features @@ -28,9 +28,26 @@ Papercut SMTP UI Requires the "WebView2" Microsoft shared system component be in #### Logging View ![Logging View](https://changemakerstudios.us/content/images/2020/07/Papercut-Log.png) -## Papercut SMTP Background Service -Papercut SMTP has an optional "always on" service to receive emails even when the client is not running. It's installed by default with [Papercut.Setup.exe](https://github.com/ChangemakerStudios/Papercut/releases). +## Papercut SMTP Service +Papercut SMTP has an optional HTTP server to receive emails even when the client is not running. It's installed by default with [Papercut.Setup.exe](https://github.com/ChangemakerStudios/Papercut/releases). Alternatively, it can be run in an almost portable way by downloading [Papercut.Service.zip](https://github.com/ChangemakerStudios/Papercut/releases), unzipping and [following the service installation instructions](https://github.com/ChangemakerStudios/Papercut/tree/develop/src/Papercut.Service). +### Host in Docker + +Optionally you can run Papercut SMTP Service in docker: [Papercut SMTP Service in Docker](https://hub.docker.com/r/changemakerstudiosus/papercut-smtp) + +#### Pull Image: + +```powershell +> docker pull changemakerstudiosus/papercut-smtp:latest +``` + +#### Run Papercut STMP Server Locally in Docker (HTTP Port :8080 and STMP port 25) +```powershell +docker run -d -p 8080:80 -p 25:25 changemakerstudiosus/papercut-smtp:latest +``` + +The Papercut-SMTP Server Site will be accessible at http://localhost:8080. + ## License Papercut SMTP is Licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). diff --git a/ReleaseNotes.md b/ReleaseNotes.md index eadb8b14..83e89b99 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,6 +1,6 @@ # Release Notes -## Papercut SMTP v7.0.0 [2024-05-17] +## Papercut SMTP v7.0.0 [2024-10-14] _NOTE: Uninstall any existing Papercut SMTP installations BEFORE installing this new version._ @@ -8,6 +8,8 @@ _NOTE: Uninstall any existing Papercut SMTP installations BEFORE installing this - Upgraded to latest dependencies (Caliburn Micro, Autofac, MahApps) and associated systems to support .NET 8. - Switched to [Velopack](https://github.com/velopack/velopack) auto-upgradable installation system. Great project! (Thanks, [caesay](https://github.com/caesay)!) - Fix for log updating constantly causing unnecessary WebView2 loading. (PR thanks to [arthurzaczek](https://github.com/arthurzaczek)) +- Fix for double request when clicking on links in emails. Issue #232. +- Added InvokeProcess Rule (#274) ## Papercut SMTP v6.2.0 [2022-04-24] diff --git a/build-docker.ps1 b/build-docker.ps1 new file mode 100644 index 00000000..e427ba3b --- /dev/null +++ b/build-docker.ps1 @@ -0,0 +1,17 @@ +param ( + [string]$BuildVersion +) + +if (-not $BuildVersion) { + Write-Host "You must specify a build version. E.g.: 7.0.1" + exit 1 +} + +$BuildDate = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ") +$VcsRef = (git rev-parse --short HEAD) + +docker build -t "changemakerstudiosus/papercut-smtp:$BuildVersion" . ` + --build-arg BUILD_VERSION=$BuildVersion ` + --build-arg BUILD_DATE=$BuildDate ` + --build-arg VCS_REF=$VcsRef ` + --no-cache \ No newline at end of file diff --git a/build-docker.sh b/build-docker.sh new file mode 100644 index 00000000..f929709f --- /dev/null +++ b/build-docker.sh @@ -0,0 +1,14 @@ +BuildVersion=$1 +if [ -z "$BuildVersion" ]; then + echo "You must specify a build version. E.g.: 7.0.1" + exit 1 +fi + +BuildDate=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +VcsRef=$(git rev-parse --short HEAD) + +docker build -t changemakerstudiosus/papercut-smtp:"$BuildVersion" . \ + --build-arg BUILD_VERSION="$BuildVersion" \ + --build-arg BUILD_DATE="$BuildDate" \ + --build-arg VCS_REF="$VcsRef" \ + --no-cache \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 0738f701..06add0bd 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,3 +1,3 @@ dotnet tool install --global Cake.Tool --version 3.2.0 -dotnet tool install --global vpk --version 0.0.359 +dotnet tool install --global vpk --version 0.0.626 dotnet-cake --configuration=Release \ No newline at end of file diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs index ca84b9d4..4e4c04d2 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/src/GlobalAssemblyInfo.cs @@ -5,6 +5,6 @@ //------------------------------------------------------------------------------ using System.Reflection; -[assembly: AssemblyVersion("7.0.0.0")] -[assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0.0")] \ No newline at end of file +[assembly: AssemblyVersion("7.1.0.0")] +[assembly: AssemblyFileVersion("7.1.0.0")] +[assembly: AssemblyInformationalVersion("7.1.0-dev.111+Branch.develop.Sha.c6c2a6557ac3b3a8d1ebd93f45de2ff515018f18")] \ No newline at end of file diff --git a/src/Papercut.Common/Papercut.Common.csproj b/src/Papercut.Common/Papercut.Common.csproj index 38a0f8f2..832d475a 100644 --- a/src/Papercut.Common/Papercut.Common.csproj +++ b/src/Papercut.Common/Papercut.Common.csproj @@ -5,7 +5,12 @@ - + + + + + + \ No newline at end of file diff --git a/src/Papercut.Core/AppConstants.cs b/src/Papercut.Core/AppConstants.cs index 1a2c8efc..bcf2d9c4 100644 --- a/src/Papercut.Core/AppConstants.cs +++ b/src/Papercut.Core/AppConstants.cs @@ -24,6 +24,8 @@ public static class AppConstants public const string CompanyName = "Changemaker Studios"; + public const string UpgradeUrl = "https://github.com/ChangemakerStudios/Papercut-SMTP"; + public static string AppDataDirectory { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), CompanyName, diff --git a/src/Papercut.Core/Domain/Application/ApplicationMeta.cs b/src/Papercut.Core/Domain/Application/ApplicationMeta.cs index 63e7f590..8ece1bf8 100644 --- a/src/Papercut.Core/Domain/Application/ApplicationMeta.cs +++ b/src/Papercut.Core/Domain/Application/ApplicationMeta.cs @@ -20,16 +20,10 @@ namespace Papercut.Core.Domain.Application { - public class ApplicationMeta : IAppMeta + public class ApplicationMeta(string appName, string? appVersion = null) : IAppMeta { - public ApplicationMeta(string appName, string? appVersion = null) - { - this.AppName = appName; - this.AppVersion = appVersion ?? Assembly.GetCallingAssembly().GetName().Version.ToString(3); - } + public string AppName { get; } = appName; - public string AppName { get; } - - public string AppVersion { get; } + public string AppVersion { get; } = appVersion ?? Assembly.GetCallingAssembly().GetName().Version?.ToString(3) ?? "1.0.0.0"; } } diff --git a/src/Papercut.Core/Infrastructure/Container/RegisterMethodExtensions.cs b/src/Papercut.Core/Infrastructure/Container/RegisterMethodExtensions.cs index a4975db2..d1499c6f 100644 --- a/src/Papercut.Core/Infrastructure/Container/RegisterMethodExtensions.cs +++ b/src/Papercut.Core/Infrastructure/Container/RegisterMethodExtensions.cs @@ -59,7 +59,7 @@ public static IReadOnlyList> GetStaticRegisterMethods(t z => new Action( c => { - Log.Debug( + Log.Verbose( "Invoking Registration Method {MethodType} {MethodName}", z.DeclaringType?.ToString(), z.ToString()); diff --git a/src/Papercut.Core/Infrastructure/Logging/LoggerExtensions.cs b/src/Papercut.Core/Infrastructure/Logging/LoggerExtensions.cs new file mode 100644 index 00000000..dd749ae7 --- /dev/null +++ b/src/Papercut.Core/Infrastructure/Logging/LoggerExtensions.cs @@ -0,0 +1,31 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +namespace Papercut.Core.Infrastructure.Logging; + +public static class LoggerExtensions +{ + public static bool ErrorWithContext(this ILogger logger, Exception? exception, string message, params object?[] propertyValues) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + logger.Error(exception, message, propertyValues); + + return true; + } +} \ No newline at end of file diff --git a/src/Papercut.Core/Papercut.Core.csproj b/src/Papercut.Core/Papercut.Core.csproj index 7e39a1a3..d1c5a514 100644 --- a/src/Papercut.Core/Papercut.Core.csproj +++ b/src/Papercut.Core/Papercut.Core.csproj @@ -10,9 +10,9 @@ - 8.0.0 + 8.1.1 - + 5.0.0 @@ -20,12 +20,17 @@ 13.0.3 - - - - + + + + - + + + + + + \ No newline at end of file diff --git a/src/Papercut.Infrastructure.IPComm/Papercut.Infrastructure.IPComm.csproj b/src/Papercut.Infrastructure.IPComm/Papercut.Infrastructure.IPComm.csproj index 8001a993..e7000dcd 100644 --- a/src/Papercut.Infrastructure.IPComm/Papercut.Infrastructure.IPComm.csproj +++ b/src/Papercut.Infrastructure.IPComm/Papercut.Infrastructure.IPComm.csproj @@ -13,11 +13,16 @@ - 8.0.0 + 8.1.1 13.0.3 - + + + + + + \ No newline at end of file diff --git a/src/Papercut.Infrastructure.Smtp/Papercut.Infrastructure.Smtp.csproj b/src/Papercut.Infrastructure.Smtp/Papercut.Infrastructure.Smtp.csproj index f4bcfe47..47d3ebe4 100644 --- a/src/Papercut.Infrastructure.Smtp/Papercut.Infrastructure.Smtp.csproj +++ b/src/Papercut.Infrastructure.Smtp/Papercut.Infrastructure.Smtp.csproj @@ -14,4 +14,9 @@ + + + + + diff --git a/src/Papercut.Message/Papercut.Message.csproj b/src/Papercut.Message/Papercut.Message.csproj index 59bc6ff4..5a80c73a 100644 --- a/src/Papercut.Message/Papercut.Message.csproj +++ b/src/Papercut.Message/Papercut.Message.csproj @@ -13,15 +13,20 @@ - 8.0.0 + 8.1.1 - 4.4.0 + 4.8.0 - 4.4.0 + 4.8.0 - + + + + + + \ No newline at end of file diff --git a/src/Papercut.Rules/App/Conditional/ConditionalRuleExtensions.cs b/src/Papercut.Rules/App/Conditional/ConditionalRuleExtensions.cs new file mode 100644 index 00000000..01606b0a --- /dev/null +++ b/src/Papercut.Rules/App/Conditional/ConditionalRuleExtensions.cs @@ -0,0 +1,59 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using System.Text.RegularExpressions; + +using MimeKit; +using Papercut.Common.Helper; +using Papercut.Rules.Domain.Conditional; + +namespace Papercut.Rules.App.Conditional; + +public static class ConditionalRuleExtensions +{ + public static bool IsConditionalForwardRuleMatch(this IConditionalRule rule, MimeMessage mimeMessage) + { + if (rule == null) throw new ArgumentNullException(nameof(rule)); + if (mimeMessage == null) throw new ArgumentNullException(nameof(mimeMessage)); + + if (rule.RegexHeaderMatch.IsSet()) + { + var allHeaders = string.Join("\r\n", mimeMessage.Headers.Select(h => h.ToString())); + + if (!IsMatch(rule.RegexHeaderMatch, allHeaders)) + return false; + } + + if (rule.RegexBodyMatch.IsSet()) + { + var bodyText = string.Join("\r\n", + mimeMessage.BodyParts.OfType().Where(s => !s.IsAttachment)); + + if (!IsMatch(rule.RegexBodyMatch, bodyText)) + return false; + } + + return true; + } + + private static bool IsMatch(string match, string searchText) + { + var regex = new Regex(match, RegexOptions.IgnoreCase); + return regex.IsMatch(searchText); + } +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/Conditional/Forwarding/ConditionalForwardRuleDispatch.cs b/src/Papercut.Rules/App/Conditional/Forwarding/ConditionalForwardRuleDispatch.cs new file mode 100644 index 00000000..b1fcd9b0 --- /dev/null +++ b/src/Papercut.Rules/App/Conditional/Forwarding/ConditionalForwardRuleDispatch.cs @@ -0,0 +1,57 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using Autofac; + +using MimeKit; +using Papercut.Core.Domain.Rules; +using Papercut.Message; +using Papercut.Rules.App.Relaying; +using Papercut.Rules.Domain.Conditional.Forwarding; + +namespace Papercut.Rules.App.Conditional.Forwarding; + +public class ConditionalForwardRuleDispatch : BaseRelayRuleDispatch +{ + public ConditionalForwardRuleDispatch(Lazy mimeMessageLoader, ILogger logger) + : base(mimeMessageLoader, logger) + { + } + + protected override bool RuleMatches(ConditionalForwardRule rule, MimeMessage mimeMessage) + { + return rule.IsConditionalForwardRuleMatch(mimeMessage); + } + + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.RegisterType() + .As>().InstancePerDependency(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/Conditional/Forwarding/ConditionalForwardWithRetryRuleDispatch.cs b/src/Papercut.Rules/App/Conditional/Forwarding/ConditionalForwardWithRetryRuleDispatch.cs new file mode 100644 index 00000000..1f0a0ce5 --- /dev/null +++ b/src/Papercut.Rules/App/Conditional/Forwarding/ConditionalForwardWithRetryRuleDispatch.cs @@ -0,0 +1,106 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using Autofac; + +using MimeKit; +using Papercut.Core.Domain.Message; +using Papercut.Core.Domain.Rules; +using Papercut.Message; +using Papercut.Message.Helpers; +using Papercut.Rules.App.Relaying; +using Papercut.Rules.Domain.Conditional.Forwarding; + +using Polly; + +namespace Papercut.Rules.App.Conditional.Forwarding; + +public class ConditionalForwardWithRetryRuleDispatch : IRuleDispatcher +{ + private readonly ILogger _logger; + + private readonly Lazy _mimeMessageLoader; + + public ConditionalForwardWithRetryRuleDispatch(Lazy mimeMessageLoader, ILogger logger) + { + _mimeMessageLoader = mimeMessageLoader; + _logger = logger; + } + + public async Task DispatchAsync(ConditionalForwardWithRetryRule rule, MessageEntry messageEntry, CancellationToken token) + { + if (rule == null) throw new ArgumentNullException(nameof(rule)); + if (messageEntry == null) throw new ArgumentNullException(nameof(messageEntry)); + + var message = await _mimeMessageLoader.Value.GetClonedAsync(messageEntry, token); + + if (!RuleMatches(rule, message)) + { + return; + } + + rule.PopulateFromRule(message); + + var polly = Policy + .Handle() + .WaitAndRetryAsync( + rule.RetryAttempts, + (attempt) => TimeSpan.FromSeconds(rule.RetryAttemptDelaySeconds), + (exception, span) => + { + _logger.Error( + exception, + "Failed to send {@MessageEntry} after {RetryAttempts}", + messageEntry, + rule.RetryAttempts); + }); + + async Task SendMessage() + { + using (var client = await rule.CreateConnectedSmtpClientAsync(token)) + { + await client.SendAsync(message, token); + await client.DisconnectAsync(true, token); + } + } + + await polly.ExecuteAsync(async () => await SendMessage()); + } + + protected virtual bool RuleMatches(ConditionalForwardWithRetryRule rule, MimeMessage mimeMessage) + { + return rule.IsConditionalForwardRuleMatch(mimeMessage); + } + + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.RegisterType() + .As>().AsSelf().InstancePerDependency(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/Conditional/Relaying/ConditionalRelayRuleDispatch.cs b/src/Papercut.Rules/App/Conditional/Relaying/ConditionalRelayRuleDispatch.cs new file mode 100644 index 00000000..ac9dd272 --- /dev/null +++ b/src/Papercut.Rules/App/Conditional/Relaying/ConditionalRelayRuleDispatch.cs @@ -0,0 +1,57 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using Autofac; + +using MimeKit; +using Papercut.Core.Domain.Rules; +using Papercut.Message; +using Papercut.Rules.App.Relaying; +using Papercut.Rules.Domain.Conditional.Relaying; + +namespace Papercut.Rules.App.Conditional.Relaying; + +public class ConditionalRelayRuleDispatch : BaseRelayRuleDispatch +{ + public ConditionalRelayRuleDispatch(Lazy mimeMessageLoader, ILogger logger) + : base(mimeMessageLoader, logger) + { + } + + protected override bool RuleMatches(ConditionalRelayRule rule, MimeMessage mimeMessage) + { + return rule.IsConditionalForwardRuleMatch(mimeMessage); + } + + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.RegisterType() + .As>().AsSelf().InstancePerDependency(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/Forwarding/ForwardRuleDispatch.cs b/src/Papercut.Rules/App/Forwarding/ForwardRuleDispatch.cs new file mode 100644 index 00000000..f0c3b0ec --- /dev/null +++ b/src/Papercut.Rules/App/Forwarding/ForwardRuleDispatch.cs @@ -0,0 +1,50 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using Autofac; +using Papercut.Core.Domain.Rules; +using Papercut.Message; +using Papercut.Rules.App.Relaying; +using Papercut.Rules.Domain.Forwarding; + +namespace Papercut.Rules.App.Forwarding; + +public class ForwardRuleDispatch : BaseRelayRuleDispatch +{ + public ForwardRuleDispatch(Lazy mimeMessageLoader, ILogger logger) + : base(mimeMessageLoader, logger) + { + } + + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.RegisterType() + .As>().AsSelf().InstancePerDependency(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/Invoking/InvokeProcessRuleDispatch.cs b/src/Papercut.Rules/App/Invoking/InvokeProcessRuleDispatch.cs new file mode 100644 index 00000000..ab013520 --- /dev/null +++ b/src/Papercut.Rules/App/Invoking/InvokeProcessRuleDispatch.cs @@ -0,0 +1,110 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics; + +using Autofac; + +using Papercut.Core.Domain.Message; +using Papercut.Core.Domain.Rules; +using Papercut.Core.Infrastructure.Logging; +using Papercut.Rules.Domain.Invoking; + +using Serilog.Context; + +namespace Papercut.Rules.App.Invoking; + +public class InvokeProcessRuleDispatch(ILogger logger) : IRuleDispatcher +{ + public async Task DispatchAsync(InvokeProcessRule rule, MessageEntry messageEntry, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(rule.ProcessToRun)) + { + logger.Warning("Invoke Process Rule 'Process to Run' is not set -- nothing done"); + + return; + } + + try + { + var arguments = + (rule.ProcessCommandLine ?? string.Empty).Replace("%e", messageEntry.File); + + using var _01 = LogContext.PushProperty("Arguments", arguments); + using var _02 = LogContext.PushProperty("ProcessToRun", rule.ProcessToRun); + + using var process = new Process(); + + process.StartInfo.FileName = rule.ProcessToRun; + process.StartInfo.Arguments = arguments; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + + if (!process.Start()) + { + logger.Error("Process {ProcessToRun} Failed to Start", rule.ProcessToRun); + + return; + } + + await process.WaitForExitAsync(token); + + string output = await process.StandardOutput.ReadToEndAsync(token); + string error = await process.StandardError.ReadToEndAsync(token); + + if (process.ExitCode != 0) + { + logger.Warning( + "Process {ProcessToRun} Process failed with exit code {ExitCode}: {Error}", + rule.ProcessToRun, process.ExitCode, error); + } + + if (!string.IsNullOrWhiteSpace(output)) + { + logger.Information( + "Process {ProcessToRun} Ran Successfully. Output {ProcessOutput}", + rule.ProcessToRun, output); + } + } + catch (OperationCanceledException) + { + // ignore + } + catch (Exception ex) when (logger.ErrorWithContext(ex, "Invoke Process Rule Failed to Start Process")) + { + } + } + + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.RegisterType() + .As>().AsSelf().InstancePerDependency(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/Relaying/BaseRelayRuleDispatch.cs b/src/Papercut.Rules/App/Relaying/BaseRelayRuleDispatch.cs new file mode 100644 index 00000000..de5b8867 --- /dev/null +++ b/src/Papercut.Rules/App/Relaying/BaseRelayRuleDispatch.cs @@ -0,0 +1,75 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using MimeKit; +using Papercut.Core.Domain.Message; +using Papercut.Core.Domain.Rules; +using Papercut.Message; +using Papercut.Message.Helpers; +using Papercut.Rules.Domain.Relaying; + +namespace Papercut.Rules.App.Relaying; + +public abstract class BaseRelayRuleDispatch : IRuleDispatcher + where T : RelayRule +{ + private readonly Lazy _mimeMessageLoader; + + protected BaseRelayRuleDispatch(Lazy mimeMessageLoader, ILogger logger) + { + Logger = logger; + _mimeMessageLoader = mimeMessageLoader; + } + + protected ILogger Logger { get; } + + public virtual async Task DispatchAsync(T rule, MessageEntry messageEntry, CancellationToken token = default) + { + if (rule == null) throw new ArgumentNullException(nameof(rule)); + if (messageEntry == null) throw new ArgumentNullException(nameof(messageEntry)); + + var mimeMessage = await _mimeMessageLoader.Value.GetClonedAsync(messageEntry, token); + + if (!RuleMatches(rule, mimeMessage)) return; + + try + { + using var client = await rule.CreateConnectedSmtpClientAsync(token); + + rule.PopulateFromRule(mimeMessage); + + await client.SendAsync(mimeMessage, token); + await client.DisconnectAsync(true, token); + } + catch (Exception ex) + { + HandleSendFailure(rule, messageEntry, ex); + } + } + + protected virtual bool RuleMatches(T rule, MimeMessage mimeMessage) + { + return true; + } + + protected virtual void HandleSendFailure(T rule, MessageEntry messageEntry, Exception exception) + { + // log failure + Logger.Error(exception, "Failure sending {@MessageEntry} for rule {@Rule}", messageEntry, rule); + } +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/Relaying/RelayRuleDispatch.cs b/src/Papercut.Rules/App/Relaying/RelayRuleDispatch.cs new file mode 100644 index 00000000..f9d34a6f --- /dev/null +++ b/src/Papercut.Rules/App/Relaying/RelayRuleDispatch.cs @@ -0,0 +1,50 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using Autofac; +using Papercut.Core.Domain.Rules; +using Papercut.Message; +using Papercut.Rules.Domain.Relaying; + +namespace Papercut.Rules.App.Relaying; + +[UsedImplicitly] +public class RelayRuleDispatch : BaseRelayRuleDispatch +{ + public RelayRuleDispatch(Lazy mimeMessageLoader, ILogger logger) + : base(mimeMessageLoader, logger) + { + } + + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.RegisterType() + .As>().AsSelf().InstancePerDependency(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/Relaying/RelayRuleExtensions.cs b/src/Papercut.Rules/App/Relaying/RelayRuleExtensions.cs new file mode 100644 index 00000000..eaab918e --- /dev/null +++ b/src/Papercut.Rules/App/Relaying/RelayRuleExtensions.cs @@ -0,0 +1,62 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using MailKit.Net.Smtp; +using MailKit.Security; +using Papercut.Rules.Domain.Relaying; + +namespace Papercut.Rules.App.Relaying; + +public static class RelayRuleExtensions +{ + public static void PopulateServerFromUri(this RelayRule rule, string smtpServer) + { + if (rule == null) throw new ArgumentNullException(nameof(rule)); + + var uri = new Uri("smtp://" + smtpServer); + + rule.SmtpServer = uri.DnsSafeHost; + rule.SmtpPort = uri.IsDefaultPort ? 25 : uri.Port; + } + + public static async Task CreateConnectedSmtpClientAsync(this RelayRule forwardRule, CancellationToken token) + { + if (forwardRule == null) throw new ArgumentNullException(nameof(forwardRule)); + + var client = new SmtpClient(); + + await client.ConnectAsync( + forwardRule.SmtpServer, + forwardRule.SmtpPort, + forwardRule.SmtpUseSSL ? SecureSocketOptions.Auto : SecureSocketOptions.None, + token); + + // Note: since we don't have an OAuth2 token, disable + // the XOAUTH2 authentication mechanism. + client.AuthenticationMechanisms.Remove("XOAUTH2"); + + if (!string.IsNullOrWhiteSpace(forwardRule.SmtpPassword) + && !string.IsNullOrWhiteSpace(forwardRule.SmtpUsername)) + { + // Note: only needed if the SMTP server requires authentication + await client.AuthenticateAsync(forwardRule.SmtpUsername, forwardRule.SmtpPassword, token); + } + + return client; + } +} \ No newline at end of file diff --git a/src/Papercut.Rules/App/RuleServiceBase.cs b/src/Papercut.Rules/App/RuleServiceBase.cs index 47f7e46c..fd35ec7a 100644 --- a/src/Papercut.Rules/App/RuleServiceBase.cs +++ b/src/Papercut.Rules/App/RuleServiceBase.cs @@ -27,69 +27,68 @@ using Papercut.Core.Domain.Rules; using Papercut.Rules.Domain.Rules; -namespace Papercut.Rules.App +namespace Papercut.Rules.App; + +public class RuleServiceBase : Disposable { - public class RuleServiceBase : Disposable - { - protected readonly ILogger _logger; + protected readonly ILogger _logger; - protected readonly IRuleRepository _ruleRepository; + protected readonly IRuleRepository _ruleRepository; - readonly Lazy> _rules; + readonly Lazy> _rules; - protected RuleServiceBase(IRuleRepository ruleRepository, ILogger logger) - { - this._ruleRepository = ruleRepository; - this._logger = logger; - this.RuleFileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rules.json"); - this._rules = new Lazy>(this.GetRulesCollection); - } + protected RuleServiceBase(IRuleRepository ruleRepository, ILogger logger) + { + this._ruleRepository = ruleRepository; + this._logger = logger; + this.RuleFileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rules.json"); + this._rules = new Lazy>(this.GetRulesCollection); + } + + public string RuleFileName { get; set; } - public string RuleFileName { get; set; } + public ObservableCollection Rules => this._rules.Value; - public ObservableCollection Rules => this._rules.Value; + public IObservable> GetRuleChangedObservable(IScheduler? scheduler = null) + { + return Observable + .FromEventPattern( + h => new NotifyCollectionChangedEventHandler(h), + h => this._rules.Value.CollectionChanged += h, + h => this._rules.Value.CollectionChanged -= h, + scheduler ?? Scheduler.Default); + } - public IObservable> GetRuleChangedObservable(IScheduler? scheduler = null) + protected virtual ObservableCollection GetRulesCollection() + { + IList? loadRules = null; + + try { - return Observable - .FromEventPattern( - h => new NotifyCollectionChangedEventHandler(h), - h => this._rules.Value.CollectionChanged += h, - h => this._rules.Value.CollectionChanged -= h, - scheduler ?? Scheduler.Default); + loadRules = this._ruleRepository.LoadRules(this.RuleFileName); } - - protected virtual ObservableCollection GetRulesCollection() + catch (Exception ex) { - IList? loadRules = null; - - try - { - loadRules = this._ruleRepository.LoadRules(this.RuleFileName); - } - catch (Exception ex) - { - this._logger.Warning(ex, "Failed to load rules in file {RuleFileName}", this.RuleFileName); - } - - return new ObservableCollection(loadRules ?? new List(0)); + this._logger.Warning(ex, "Failed to load rules in file {RuleFileName}", this.RuleFileName); } - public void Save() + return new ObservableCollection(loadRules ?? new List(0)); + } + + public void Save() + { + try + { + this._ruleRepository.SaveRules(this.Rules, this.RuleFileName); + this._logger.Information( + "Saved {RuleCount} to {RuleFileName}", + this.Rules.Count, + this.RuleFileName); + } + catch (Exception ex) { - try - { - this._ruleRepository.SaveRules(this.Rules, this.RuleFileName); - this._logger.Information( - "Saved {RuleCount} to {RuleFileName}", - this.Rules.Count, - this.RuleFileName); - } - catch (Exception ex) - { - this._logger.Error(ex, "Error saving rules to file {RuleFileName}", this.RuleFileName); - } + this._logger.Error(ex, "Error saving rules to file {RuleFileName}", this.RuleFileName); } } } \ No newline at end of file diff --git a/src/Papercut.Rules/App/RulesRunner.cs b/src/Papercut.Rules/App/RulesRunner.cs index 28f4f0db..40f4f7d9 100644 --- a/src/Papercut.Rules/App/RulesRunner.cs +++ b/src/Papercut.Rules/App/RulesRunner.cs @@ -17,90 +17,99 @@ using System.Reflection; - using Autofac; using Papercut.Core.Domain.Message; using Papercut.Core.Domain.Rules; -namespace Papercut.Rules.App +namespace Papercut.Rules.App; + +public class RulesRunner : IRulesRunner { - public class RulesRunner : IRulesRunner - { - readonly MethodInfo _dispatchRuleMethod; + readonly MethodInfo _dispatchRuleMethod; - readonly ILifetimeScope _lifetimeScope; + readonly ILifetimeScope _lifetimeScope; - readonly ILogger _logger; + readonly ILogger _logger; - public RulesRunner(ILifetimeScope lifetimeScope, ILogger logger) + public RulesRunner(ILifetimeScope lifetimeScope, ILogger logger) + { + this._lifetimeScope = lifetimeScope; + this._logger = logger; + var dispatchRuleMethod = this.GetType() + .GetMethod( + nameof(this.DispatchRuleAsync), + BindingFlags.NonPublic | BindingFlags.Instance); + + if (dispatchRuleMethod == null) { - this._lifetimeScope = lifetimeScope; - this._logger = logger; - this._dispatchRuleMethod = this.GetType() - .GetMethod( - nameof(this.DispatchRuleAsync), - BindingFlags.NonPublic | BindingFlags.Instance); + throw new ArgumentNullException(nameof(dispatchRuleMethod), "Dispatch rule method is null"); } - public async Task RunAsync(IRule[] rules, MessageEntry messageEntry, CancellationToken token) + this._dispatchRuleMethod = dispatchRuleMethod; + } + + public async Task RunAsync(IRule[] rules, MessageEntry messageEntry, CancellationToken token) + { + if (rules == null) throw new ArgumentNullException(nameof(rules)); + if (messageEntry == null) throw new ArgumentNullException(nameof(messageEntry)); + + var ruleTasks = new List(); + + foreach (IRule rule in rules.Where(r => r.IsEnabled)) { - if (rules == null) throw new ArgumentNullException(nameof(rules)); - if (messageEntry == null) throw new ArgumentNullException(nameof(messageEntry)); + token.ThrowIfCancellationRequested(); - var ruleTasks = new List(); + var invoke = this._dispatchRuleMethod.MakeGenericMethod(rule.GetType()).Invoke( + this, + [rule, messageEntry, token]); - foreach (IRule rule in rules.Where(_ => _.IsEnabled)) + if (invoke is Task invokeTask) { - token.ThrowIfCancellationRequested(); - - ruleTasks.Add( - (Task)this._dispatchRuleMethod.MakeGenericMethod(rule.GetType()).Invoke( - this, - new object[] { rule, messageEntry, token })); + ruleTasks.Add(invokeTask); } - - await Task.WhenAll(ruleTasks); } - [UsedImplicitly] - async Task DispatchRuleAsync(TRule rule, MessageEntry messageEntry, CancellationToken token) - where TRule : IRule + await Task.WhenAll(ruleTasks).WaitAsync(token); + } + + [UsedImplicitly] + async Task DispatchRuleAsync(TRule rule, MessageEntry messageEntry, CancellationToken token) + where TRule : IRule + { + this._logger.Information( + "Running Rule Dispatch for Rule {Rule} on Message {@MessageEntry}", + rule, + messageEntry); + + try + { + var ruleDispatcher = this._lifetimeScope.Resolve>(); + await ruleDispatcher.DispatchAsync(rule, messageEntry, token); + } + catch (Exception ex) { - this._logger.Information( - "Running Rule Dispatch for Rule {Rule} on Message {@MessageEntry}", + this._logger.Warning( + ex, + "Failure Dispatching Rule {Rule} for Message {@MessageEntry}", rule, messageEntry); - - try - { - var ruleDispatcher = this._lifetimeScope.Resolve>(); - await ruleDispatcher.DispatchAsync(rule, messageEntry, token); - } - catch (Exception ex) - { - this._logger.Warning( - ex, - "Failure Dispatching Rule {Rule} for Message {@MessageEntry}", - rule, - messageEntry); - } } + } - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + #region Begin Static Container Registrations - builder.RegisterType().As().SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); - #endregion + builder.RegisterType().As().SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Conditional/ConditionalRuleExtensions.cs b/src/Papercut.Rules/Domain/Conditional/ConditionalRuleExtensions.cs deleted file mode 100644 index ce07ef72..00000000 --- a/src/Papercut.Rules/Domain/Conditional/ConditionalRuleExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Papercut -// -// Copyright © 2008 - 2012 Ken Robertson -// Copyright © 2013 - 2024 Jaben Cargman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using System.Text.RegularExpressions; - -using MimeKit; - -using Papercut.Common.Helper; - -namespace Papercut.Rules.Domain.Conditional -{ - public static class ConditionalRuleExtensions - { - public static bool IsConditionalForwardRuleMatch(this IConditionalRule rule, MimeMessage mimeMessage) - { - if (rule == null) throw new ArgumentNullException(nameof(rule)); - if (mimeMessage == null) throw new ArgumentNullException(nameof(mimeMessage)); - - if (rule.RegexHeaderMatch.IsSet()) - { - var allHeaders = string.Join("\r\n", mimeMessage.Headers.Select(h => h.ToString())); - - if (!IsMatch(rule.RegexHeaderMatch, allHeaders)) - return false; - } - - if (rule.RegexBodyMatch.IsSet()) - { - var bodyText = string.Join("\r\n", - mimeMessage.BodyParts.OfType().Where(s => !s.IsAttachment)); - - if (!IsMatch(rule.RegexBodyMatch, bodyText)) - return false; - } - - return true; - } - - private static bool IsMatch(string match, string searchText) - { - var regex = new Regex(match, RegexOptions.IgnoreCase); - return regex.IsMatch(searchText); - } - } -} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardRule.cs b/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardRule.cs index 662ee2f0..42ab399e 100644 --- a/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardRule.cs +++ b/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardRule.cs @@ -19,70 +19,69 @@ using System.ComponentModel; using Autofac; - using Papercut.Common.Extensions; using Papercut.Common.Helper; using Papercut.Core.Domain.Rules; +using Papercut.Rules.Domain.Conditional; using Papercut.Rules.Domain.Forwarding; -namespace Papercut.Rules.Domain.Conditional.Forwarding +namespace Papercut.Rules.Domain.Conditional.Forwarding; + +[Serializable] +public class ConditionalForwardRule : ForwardRule, IConditionalRule { - [Serializable] - public class ConditionalForwardRule : ForwardRule, IConditionalRule - { - string _regexBodyMatch; + string _regexBodyMatch; - string _regexHeaderMatch; + string _regexHeaderMatch; - [Category("Information")] - public override string Type => "Conditional Forward"; + [Category("Information")] + public override string Type => "Conditional Forward"; - [DisplayName("Regex Header Match")] - public string RegexHeaderMatch + [DisplayName("Regex Header Match")] + public string RegexHeaderMatch + { + get => _regexHeaderMatch; + set { - get => this._regexHeaderMatch; - set - { - if (value == this._regexHeaderMatch) - return; - this._regexHeaderMatch = value.IsSet() && value.IsValidRegex() ? value : null; ; - this.OnPropertyChanged(nameof(this.RegexHeaderMatch)); - } + if (value == _regexHeaderMatch) + return; + _regexHeaderMatch = value.IsSet() && value.IsValidRegex() ? value : null; ; + OnPropertyChanged(nameof(RegexHeaderMatch)); } + } - [DisplayName("Regex Body Match")] - public string RegexBodyMatch + [DisplayName("Regex Body Match")] + public string RegexBodyMatch + { + get => _regexBodyMatch; + set { - get => this._regexBodyMatch; - set - { - if (value == this._regexBodyMatch) - return; - - this._regexBodyMatch = value.IsSet() && value.IsValidRegex() ? value : null; - this.OnPropertyChanged(nameof(this.RegexBodyMatch)); - } - } + if (value == _regexBodyMatch) + return; - protected override IEnumerable>> GetPropertiesForDescription() - { - return base.GetPropertiesForDescription().Concat(this.GetProperties()); + _regexBodyMatch = value.IsSet() && value.IsValidRegex() ? value : null; + OnPropertyChanged(nameof(RegexBodyMatch)); } + } - #region Begin Static Container Registrations + protected override IEnumerable>> GetPropertiesForDescription() + { + return base.GetPropertiesForDescription().Concat(this.GetProperties()); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + #region Begin Static Container Registrations - builder.RegisterType().AsSelf().As().InstancePerDependency(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); - #endregion + builder.RegisterType().AsSelf().As().InstancePerDependency(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardRuleDispatch.cs b/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardRuleDispatch.cs deleted file mode 100644 index 2c5937b5..00000000 --- a/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardRuleDispatch.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Papercut -// -// Copyright © 2008 - 2012 Ken Robertson -// Copyright © 2013 - 2024 Jaben Cargman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using Autofac; - -using MimeKit; - -using Papercut.Core.Domain.Rules; -using Papercut.Message; -using Papercut.Rules.Domain.Relaying; - -namespace Papercut.Rules.Domain.Conditional.Forwarding -{ - public class ConditionalForwardRuleDispatch : BaseRelayRuleDispatch - { - public ConditionalForwardRuleDispatch(Lazy mimeMessageLoader, ILogger logger) - : base(mimeMessageLoader, logger) - { - } - - protected override bool RuleMatches(ConditionalForwardRule rule, MimeMessage mimeMessage) - { - return rule.IsConditionalForwardRuleMatch(mimeMessage); - } - - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.RegisterType() - .As>().InstancePerDependency(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardWithRetryRule.cs b/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardWithRetryRule.cs index 05612e1c..8e60ddbb 100644 --- a/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardWithRetryRule.cs +++ b/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardWithRetryRule.cs @@ -19,74 +19,72 @@ using System.ComponentModel; using Autofac; - using Papercut.Common.Extensions; using Papercut.Common.Helper; using Papercut.Core.Domain.Rules; -namespace Papercut.Rules.Domain.Conditional.Forwarding +namespace Papercut.Rules.Domain.Conditional.Forwarding; + +[Serializable] +public class ConditionalForwardWithRetryRule : ConditionalForwardRule { - [Serializable] - public class ConditionalForwardWithRetryRule : ConditionalForwardRule - { - private int _retryAttemptDelaySeconds; + private int _retryAttemptDelaySeconds; - private int _retryAttempts; + private int _retryAttempts; - public ConditionalForwardWithRetryRule() - { - this.RetryAttempts = 5; - this.RetryAttemptDelaySeconds = 60; - } + public ConditionalForwardWithRetryRule() + { + RetryAttempts = 5; + RetryAttemptDelaySeconds = 60; + } - [Category("Settings")] - [DisplayName("Retry Attempts")] - public int RetryAttempts + [Category("Settings")] + [DisplayName("Retry Attempts")] + public int RetryAttempts + { + get => _retryAttempts; + set { - get => this._retryAttempts; - set - { - if (value == this._retryAttempts) return; - this._retryAttempts = value; - this.OnPropertyChanged(nameof(this.RetryAttempts)); - } + if (value == _retryAttempts) return; + _retryAttempts = value; + OnPropertyChanged(nameof(RetryAttempts)); } + } - [Category("Settings")] - [DisplayName("Retry Attempt Delay in Seconds")] - public int RetryAttemptDelaySeconds + [Category("Settings")] + [DisplayName("Retry Attempt Delay in Seconds")] + public int RetryAttemptDelaySeconds + { + get => _retryAttemptDelaySeconds; + set { - get => this._retryAttemptDelaySeconds; - set - { - if (value == this._retryAttemptDelaySeconds) return; - this._retryAttemptDelaySeconds = value; - this.OnPropertyChanged(nameof(this.RetryAttemptDelaySeconds)); - } + if (value == _retryAttemptDelaySeconds) return; + _retryAttemptDelaySeconds = value; + OnPropertyChanged(nameof(RetryAttemptDelaySeconds)); } + } - [Category("Information")] - public override string Type => "Conditional Forward with Retry"; + [Category("Information")] + public override string Type => "Conditional Forward with Retry"; - public override string ToString() - { - return this.GetProperties().OrderBy(s => s.Key).ToFormattedPairs().Join("\r\n"); - } - - #region Begin Static Container Registrations + public override string ToString() + { + return this.GetProperties().OrderBy(s => s.Key).ToFormattedPairs().Join("\r\n"); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + #region Begin Static Container Registrations - builder.RegisterType().AsSelf().As().InstancePerDependency(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); - #endregion + builder.RegisterType().AsSelf().As().InstancePerDependency(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardWithRetryRuleDispatch.cs b/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardWithRetryRuleDispatch.cs deleted file mode 100644 index abf6a435..00000000 --- a/src/Papercut.Rules/Domain/Conditional/Forwarding/ConditionalForwardWithRetryRuleDispatch.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Papercut -// -// Copyright © 2008 - 2012 Ken Robertson -// Copyright © 2013 - 2024 Jaben Cargman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using Autofac; - -using MimeKit; - -using Papercut.Core.Domain.Message; -using Papercut.Core.Domain.Rules; -using Papercut.Message; -using Papercut.Message.Helpers; -using Papercut.Rules.Domain.Relaying; - -using Polly; - -namespace Papercut.Rules.Domain.Conditional.Forwarding -{ - public class ConditionalForwardWithRetryRuleDispatch : IRuleDispatcher - { - private readonly ILogger _logger; - - private readonly Lazy _mimeMessageLoader; - - public ConditionalForwardWithRetryRuleDispatch(Lazy mimeMessageLoader, ILogger logger) - { - this._mimeMessageLoader = mimeMessageLoader; - this._logger = logger; - } - - public async Task DispatchAsync(ConditionalForwardWithRetryRule rule, MessageEntry messageEntry, CancellationToken token) - { - if (rule == null) throw new ArgumentNullException(nameof(rule)); - if (messageEntry == null) throw new ArgumentNullException(nameof(messageEntry)); - - var message = await this._mimeMessageLoader.Value.GetClonedAsync(messageEntry, token); - - if (!this.RuleMatches(rule, message)) - { - return; - } - - rule.PopulateFromRule(message); - - var polly = Policy - .Handle() - .WaitAndRetryAsync( - rule.RetryAttempts, - (attempt) => TimeSpan.FromSeconds(rule.RetryAttemptDelaySeconds), - (exception, span) => - { - this._logger.Error( - exception, - "Failed to send {@MessageEntry} after {RetryAttempts}", - messageEntry, - rule.RetryAttempts); - }); - - async Task SendMessage() - { - using (var client = await rule.CreateConnectedSmtpClientAsync(token)) - { - await client.SendAsync(message, token); - await client.DisconnectAsync(true, token); - } - } - - await polly.ExecuteAsync(async () => await SendMessage()); - } - - protected virtual bool RuleMatches(ConditionalForwardWithRetryRule rule, MimeMessage mimeMessage) - { - return rule.IsConditionalForwardRuleMatch(mimeMessage); - } - - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.RegisterType() - .As>().AsSelf().InstancePerDependency(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Conditional/IConditionalRule.cs b/src/Papercut.Rules/Domain/Conditional/IConditionalRule.cs index a24935d8..bb4c4336 100644 --- a/src/Papercut.Rules/Domain/Conditional/IConditionalRule.cs +++ b/src/Papercut.Rules/Domain/Conditional/IConditionalRule.cs @@ -16,12 +16,11 @@ // limitations under the License. -namespace Papercut.Rules.Domain.Conditional +namespace Papercut.Rules.Domain.Conditional; + +public interface IConditionalRule { - public interface IConditionalRule - { - string RegexHeaderMatch { get; set; } + string RegexHeaderMatch { get; set; } - string RegexBodyMatch { get; set; } - } + string RegexBodyMatch { get; set; } } \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Conditional/Relaying/ConditionalRelayRule.cs b/src/Papercut.Rules/Domain/Conditional/Relaying/ConditionalRelayRule.cs index f1a390b7..8fe823c0 100644 --- a/src/Papercut.Rules/Domain/Conditional/Relaying/ConditionalRelayRule.cs +++ b/src/Papercut.Rules/Domain/Conditional/Relaying/ConditionalRelayRule.cs @@ -19,70 +19,69 @@ using System.ComponentModel; using Autofac; - using Papercut.Common.Extensions; using Papercut.Common.Helper; using Papercut.Core.Domain.Rules; +using Papercut.Rules.Domain.Conditional; using Papercut.Rules.Domain.Relaying; -namespace Papercut.Rules.Domain.Conditional.Relaying +namespace Papercut.Rules.Domain.Conditional.Relaying; + +[Serializable] +public class ConditionalRelayRule : RelayRule, IConditionalRule { - [Serializable] - public class ConditionalRelayRule : RelayRule, IConditionalRule - { - string? _regexBodyMatch; + string? _regexBodyMatch; - string? _regexHeaderMatch; + string? _regexHeaderMatch; - [Category("Information")] - public override string Type => "Conditional Relay"; + [Category("Information")] + public override string Type => "Conditional Relay"; - [DisplayName("Regex Header Match")] - public string RegexHeaderMatch + [DisplayName("Regex Header Match")] + public string RegexHeaderMatch + { + get => _regexHeaderMatch; + set { - get => this._regexHeaderMatch; - set - { - if (value == this._regexHeaderMatch) - return; - this._regexHeaderMatch = value.IsSet() && value.IsValidRegex() ? value : null; ; - this.OnPropertyChanged(nameof(this.RegexHeaderMatch)); - } + if (value == _regexHeaderMatch) + return; + _regexHeaderMatch = value.IsSet() && value.IsValidRegex() ? value : null; ; + OnPropertyChanged(nameof(RegexHeaderMatch)); } + } - [DisplayName("Regex Body Match")] - public string RegexBodyMatch + [DisplayName("Regex Body Match")] + public string RegexBodyMatch + { + get => _regexBodyMatch; + set { - get => this._regexBodyMatch; - set - { - if (value == this._regexBodyMatch) - return; - - this._regexBodyMatch = value.IsSet() && value.IsValidRegex() ? value : null; - this.OnPropertyChanged(nameof(this.RegexBodyMatch)); - } - } + if (value == _regexBodyMatch) + return; - protected override IEnumerable>> GetPropertiesForDescription() - { - return base.GetPropertiesForDescription().Concat(this.GetProperties()); + _regexBodyMatch = value.IsSet() && value.IsValidRegex() ? value : null; + OnPropertyChanged(nameof(RegexBodyMatch)); } + } - #region Begin Static Container Registrations + protected override IEnumerable>> GetPropertiesForDescription() + { + return base.GetPropertiesForDescription().Concat(this.GetProperties()); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + #region Begin Static Container Registrations - builder.RegisterType().AsSelf().As().InstancePerDependency(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); - #endregion + builder.RegisterType().AsSelf().As().InstancePerDependency(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Conditional/Relaying/ConditionalRelayRuleDispatch.cs b/src/Papercut.Rules/Domain/Conditional/Relaying/ConditionalRelayRuleDispatch.cs deleted file mode 100644 index ee065f46..00000000 --- a/src/Papercut.Rules/Domain/Conditional/Relaying/ConditionalRelayRuleDispatch.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Papercut -// -// Copyright © 2008 - 2012 Ken Robertson -// Copyright © 2013 - 2024 Jaben Cargman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using Autofac; - -using MimeKit; - -using Papercut.Core.Domain.Rules; -using Papercut.Message; -using Papercut.Rules.Domain.Relaying; - -namespace Papercut.Rules.Domain.Conditional.Relaying -{ - public class ConditionalRelayRuleDispatch : BaseRelayRuleDispatch - { - public ConditionalRelayRuleDispatch(Lazy mimeMessageLoader, ILogger logger) - : base(mimeMessageLoader, logger) - { - } - - protected override bool RuleMatches(ConditionalRelayRule rule, MimeMessage mimeMessage) - { - return rule.IsConditionalForwardRuleMatch(mimeMessage); - } - - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.RegisterType() - .As>().AsSelf().InstancePerDependency(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Forwarding/ForwardRule.cs b/src/Papercut.Rules/Domain/Forwarding/ForwardRule.cs index 172f1ec4..af8d5750 100644 --- a/src/Papercut.Rules/Domain/Forwarding/ForwardRule.cs +++ b/src/Papercut.Rules/Domain/Forwarding/ForwardRule.cs @@ -26,87 +26,86 @@ using Papercut.Core.Domain.Rules; using Papercut.Rules.Domain.Relaying; -namespace Papercut.Rules.Domain.Forwarding +namespace Papercut.Rules.Domain.Forwarding; + +[Serializable] +public class ForwardRule : RelayRule { - [Serializable] - public class ForwardRule : RelayRule - { - private string _fromEmail; + private string _fromEmail; - private string _toEmail; + private string _toEmail; - [Category("Information")] - public override string Type => "Forward"; + [Category("Information")] + public override string Type => "Forward"; - [Category("Settings")] - [DisplayName("From Email")] - [Description("Forward From Email")] - public string FromEmail + [Category("Settings")] + [DisplayName("From Email")] + [Description("Forward From Email")] + public string FromEmail + { + get => this._fromEmail; + set { - get => this._fromEmail; - set - { - if (value == this._fromEmail) return; - this._fromEmail = value; - this.OnPropertyChanged(nameof(this.FromEmail)); - } + if (value == this._fromEmail) return; + this._fromEmail = value; + this.OnPropertyChanged(nameof(this.FromEmail)); } + } - [Category("Settings")] - [DisplayName("To Email")] - [Description("Foward To Email")] - public string ToEmail + [Category("Settings")] + [DisplayName("To Email")] + [Description("Foward To Email")] + public string ToEmail + { + get => this._toEmail; + set { - get => this._toEmail; - set - { - if (value == this._toEmail) return; - this._toEmail = value; - this.OnPropertyChanged(nameof(this.ToEmail)); - } + if (value == this._toEmail) return; + this._toEmail = value; + this.OnPropertyChanged(nameof(this.ToEmail)); } + } + + public override void PopulateFromRule(MimeMessage mimeMessage) + { + if (mimeMessage == null) throw new ArgumentNullException(nameof(mimeMessage)); - public override void PopulateFromRule(MimeMessage mimeMessage) + if (!string.IsNullOrWhiteSpace(this.FromEmail)) { - if (mimeMessage == null) throw new ArgumentNullException(nameof(mimeMessage)); - - if (!string.IsNullOrWhiteSpace(this.FromEmail)) - { - mimeMessage.From.Clear(); - mimeMessage.From.Add( - new MailboxAddress(this.FromEmail, this.FromEmail)); - } - - if (!string.IsNullOrWhiteSpace(this.ToEmail)) - { - mimeMessage.To.Clear(); - mimeMessage.Bcc.Clear(); - mimeMessage.Cc.Clear(); - mimeMessage.To.Add(new MailboxAddress(this.ToEmail, this.ToEmail)); - } - - base.PopulateFromRule(mimeMessage); + mimeMessage.From.Clear(); + mimeMessage.From.Add( + new MailboxAddress(this.FromEmail, this.FromEmail)); } - protected override IEnumerable>> GetPropertiesForDescription() + if (!string.IsNullOrWhiteSpace(this.ToEmail)) { - return base.GetPropertiesForDescription().Concat(this.GetProperties()); + mimeMessage.To.Clear(); + mimeMessage.Bcc.Clear(); + mimeMessage.Cc.Clear(); + mimeMessage.To.Add(new MailboxAddress(this.ToEmail, this.ToEmail)); } - #region Begin Static Container Registrations + base.PopulateFromRule(mimeMessage); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + protected override IEnumerable>> GetPropertiesForDescription() + { + return base.GetPropertiesForDescription().Concat(this.GetProperties()); + } - builder.RegisterType().AsSelf().As().InstancePerDependency(); - } + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); - #endregion + builder.RegisterType().AsSelf().As().InstancePerDependency(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Forwarding/ForwardRuleDispatch.cs b/src/Papercut.Rules/Domain/Forwarding/ForwardRuleDispatch.cs deleted file mode 100644 index a0ea5585..00000000 --- a/src/Papercut.Rules/Domain/Forwarding/ForwardRuleDispatch.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Papercut -// -// Copyright © 2008 - 2012 Ken Robertson -// Copyright © 2013 - 2024 Jaben Cargman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using Autofac; - -using Papercut.Core.Domain.Rules; -using Papercut.Message; -using Papercut.Rules.Domain.Relaying; - -namespace Papercut.Rules.Domain.Forwarding -{ - public class ForwardRuleDispatch : BaseRelayRuleDispatch - { - public ForwardRuleDispatch(Lazy mimeMessageLoader, ILogger logger) - : base(mimeMessageLoader, logger) - { - } - - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.RegisterType() - .As>().AsSelf().InstancePerDependency(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Invoking/InvokeProcessRule.cs b/src/Papercut.Rules/Domain/Invoking/InvokeProcessRule.cs new file mode 100644 index 00000000..451ec43e --- /dev/null +++ b/src/Papercut.Rules/Domain/Invoking/InvokeProcessRule.cs @@ -0,0 +1,88 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using System.ComponentModel; + +using Autofac; + +using MimeKit; + +using Papercut.Common.Extensions; +using Papercut.Core.Domain.Rules; +using Papercut.Rules.Domain.Rules; + +namespace Papercut.Rules.Domain.Invoking; + +[Serializable] +public class InvokeProcessRule : RuleBase +{ + private string? _processToRun; + private string? _processCommandLine = @"""%e"""; + + [Category("Information")] + public override string Type => "Invoke Process"; + + [Category("Settings")] + [DisplayName("Process to Run")] + [Description("Full Path and EXE of Process to Run on New Message")] + public string? ProcessToRun + { + get => _processToRun; + set + { + if (value == _processToRun) return; + _processToRun = value; + OnPropertyChanged(nameof(ProcessToRun)); + } + } + + [Category("Settings")] + [DisplayName("Process Command Line")] + [Description(@"Command line to use when running process. Note ""%e"" will be replaced with the full path to the email that triggered the rule.")] + public string? ProcessCommandLine + { + get => this._processCommandLine; + set + { + if (value == this._processCommandLine) return; + this._processCommandLine = value; + this.OnPropertyChanged(nameof(this.ProcessCommandLine)); + } + } + + protected override IEnumerable>> GetPropertiesForDescription() + { + return base.GetPropertiesForDescription().Concat(this.GetProperties()); + } + + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.RegisterType().AsSelf().As().InstancePerDependency(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Relaying/BaseRelayRuleDispatch.cs b/src/Papercut.Rules/Domain/Relaying/BaseRelayRuleDispatch.cs deleted file mode 100644 index 7cead752..00000000 --- a/src/Papercut.Rules/Domain/Relaying/BaseRelayRuleDispatch.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Papercut -// -// Copyright © 2008 - 2012 Ken Robertson -// Copyright © 2013 - 2024 Jaben Cargman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using MimeKit; - -using Papercut.Core.Domain.Message; -using Papercut.Core.Domain.Rules; -using Papercut.Message; -using Papercut.Message.Helpers; - -namespace Papercut.Rules.Domain.Relaying -{ - public abstract class BaseRelayRuleDispatch : IRuleDispatcher - where T : RelayRule - { - private readonly Lazy _mimeMessageLoader; - - protected BaseRelayRuleDispatch(Lazy mimeMessageLoader, ILogger logger) - { - this.Logger = logger; - this._mimeMessageLoader = mimeMessageLoader; - } - - protected ILogger Logger { get; } - - public virtual async Task DispatchAsync(T rule, MessageEntry messageEntry, CancellationToken token = default) - { - if (rule == null) throw new ArgumentNullException(nameof(rule)); - if (messageEntry == null) throw new ArgumentNullException(nameof(messageEntry)); - - var mimeMessage = await this._mimeMessageLoader.Value.GetClonedAsync(messageEntry, token); - - if (!this.RuleMatches(rule, mimeMessage)) return; - - try - { - using (var client = await rule.CreateConnectedSmtpClientAsync(token)) - { - rule.PopulateFromRule(mimeMessage); - await client.SendAsync(mimeMessage, token); - await client.DisconnectAsync(true, token); - } - } - catch (Exception ex) - { - this.HandleSendFailure(rule, messageEntry, ex); - } - } - - protected virtual bool RuleMatches(T rule, MimeMessage mimeMessage) - { - return true; - } - - protected virtual void HandleSendFailure(T rule, MessageEntry messageEntry, Exception exception) - { - // log failure - this.Logger.Error(exception, "Failure sending {@MessageEntry} for rule {@Rule}", messageEntry, rule); - } - } -} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Relaying/RelayRule.cs b/src/Papercut.Rules/Domain/Relaying/RelayRule.cs index eb930b52..6e56b6fd 100644 --- a/src/Papercut.Rules/Domain/Relaying/RelayRule.cs +++ b/src/Papercut.Rules/Domain/Relaying/RelayRule.cs @@ -26,140 +26,139 @@ using Papercut.Core.Domain.Rules; using Papercut.Rules.Domain.Rules; -namespace Papercut.Rules.Domain.Relaying +namespace Papercut.Rules.Domain.Relaying; + +[Serializable] +public class RelayRule : RuleBase { - [Serializable] - public class RelayRule : RuleBase - { - string _smtpPassword; + string _smtpPassword; - int _smtpPort = 25; + int _smtpPort = 25; - string _smtpServer = "10.0.0.1"; + string _smtpServer = "10.0.0.1"; - string _smtpUsername; + string _smtpUsername; - bool _smtpUseSsl; + bool _smtpUseSsl; - private string _toBcc; + private string _toBcc; - [Category("Settings")] - [DisplayName("SMTP Password")] - [PasswordPropertyText] - public string SmtpPassword + [Category("Settings")] + [DisplayName("SMTP Password")] + [PasswordPropertyText] + public string SmtpPassword + { + get => this._smtpPassword; + set { - get => this._smtpPassword; - set - { - if (value == this._smtpPassword) return; - this._smtpPassword = value; - this.OnPropertyChanged(nameof(this.SmtpPassword)); - } + if (value == this._smtpPassword) return; + this._smtpPassword = value; + this.OnPropertyChanged(nameof(this.SmtpPassword)); } + } - [Category("Settings")] - [DisplayName("SMTP Username")] - public string SmtpUsername + [Category("Settings")] + [DisplayName("SMTP Username")] + public string SmtpUsername + { + get => this._smtpUsername; + set { - get => this._smtpUsername; - set - { - if (value == this._smtpUsername) return; - this._smtpUsername = value; - this.OnPropertyChanged(nameof(this.SmtpUsername)); - } + if (value == this._smtpUsername) return; + this._smtpUsername = value; + this.OnPropertyChanged(nameof(this.SmtpUsername)); } + } - [Category("Settings")] - [DisplayName("SMTP Port")] - public int SmtpPort + [Category("Settings")] + [DisplayName("SMTP Port")] + public int SmtpPort + { + get => this._smtpPort; + set { - get => this._smtpPort; - set - { - if (value == this._smtpPort) return; - this._smtpPort = value; - this.OnPropertyChanged(nameof(this.SmtpPort)); - } + if (value == this._smtpPort) return; + this._smtpPort = value; + this.OnPropertyChanged(nameof(this.SmtpPort)); } + } - [Category("Settings")] - [DisplayName("SMTP Use SSL")] - public bool SmtpUseSSL + [Category("Settings")] + [DisplayName("SMTP Use SSL")] + public bool SmtpUseSSL + { + get => this._smtpUseSsl; + set { - get => this._smtpUseSsl; - set - { - if (value.Equals(this._smtpUseSsl)) return; - this._smtpUseSsl = value; - this.OnPropertyChanged(nameof(this.SmtpUseSSL)); - } + if (value.Equals(this._smtpUseSsl)) return; + this._smtpUseSsl = value; + this.OnPropertyChanged(nameof(this.SmtpUseSSL)); } + } - [Category("Information")] - public override string Type => "Relay"; + [Category("Information")] + public override string Type => "Relay"; - [Category("Settings")] - [DisplayName("SMTP Server")] - public string SmtpServer + [Category("Settings")] + [DisplayName("SMTP Server")] + public string SmtpServer + { + get => this._smtpServer; + set { - get => this._smtpServer; - set - { - if (value == this._smtpServer) return; - this._smtpServer = value; - this.OnPropertyChanged(nameof(this.SmtpServer)); - } + if (value == this._smtpServer) return; + this._smtpServer = value; + this.OnPropertyChanged(nameof(this.SmtpServer)); } + } - [Category("Settings")] - [DisplayName("To Bcc Email(s)")] - [Description("To Bcc Email(s) (comma delimited)")] - public string ToBcc + [Category("Settings")] + [DisplayName("To Bcc Email(s)")] + [Description("To Bcc Email(s) (comma delimited)")] + public string ToBcc + { + get => this._toBcc; + set { - get => this._toBcc; - set - { - if (value == this._toBcc) return; - this._toBcc = value; - this.OnPropertyChanged(nameof(this.ToBcc)); - } + if (value == this._toBcc) return; + this._toBcc = value; + this.OnPropertyChanged(nameof(this.ToBcc)); } + } - public override void PopulateFromRule(MimeMessage message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - - if (string.IsNullOrWhiteSpace(this.ToBcc)) - { - return; - } + public virtual void PopulateFromRule(MimeMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); - foreach (var bcc in this.ToBcc.Split(new[] { ',', '|', ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim())) - { - message.Bcc.Add(new MailboxAddress(bcc, bcc)); - } + if (string.IsNullOrWhiteSpace(this.ToBcc)) + { + return; } - protected override IEnumerable>> GetPropertiesForDescription() + foreach (var bcc in this.ToBcc.Split(new[] { ',', '|', ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim())) { - return base.GetPropertiesForDescription().Concat(this.GetProperties()); + message.Bcc.Add(new MailboxAddress(bcc, bcc)); } + } - #region Begin Static Container Registrations + protected override IEnumerable>> GetPropertiesForDescription() + { + return base.GetPropertiesForDescription().Concat(this.GetProperties()); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + #region Begin Static Container Registrations - builder.RegisterType().AsSelf().As().InstancePerDependency(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); - #endregion + builder.RegisterType().AsSelf().As().InstancePerDependency(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Relaying/RelayRuleDispatch.cs b/src/Papercut.Rules/Domain/Relaying/RelayRuleDispatch.cs deleted file mode 100644 index 08e24623..00000000 --- a/src/Papercut.Rules/Domain/Relaying/RelayRuleDispatch.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Papercut -// -// Copyright © 2008 - 2012 Ken Robertson -// Copyright © 2013 - 2024 Jaben Cargman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using Autofac; - -using Papercut.Core.Domain.Rules; -using Papercut.Message; - -namespace Papercut.Rules.Domain.Relaying -{ - [UsedImplicitly] - public class RelayRuleDispatch : BaseRelayRuleDispatch - { - public RelayRuleDispatch(Lazy mimeMessageLoader, ILogger logger) - : base(mimeMessageLoader, logger) - { - } - - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.RegisterType() - .As>().AsSelf().InstancePerDependency(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Relaying/RelayRuleExtensions.cs b/src/Papercut.Rules/Domain/Relaying/RelayRuleExtensions.cs deleted file mode 100644 index 9a035ae4..00000000 --- a/src/Papercut.Rules/Domain/Relaying/RelayRuleExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Papercut -// -// Copyright © 2008 - 2012 Ken Robertson -// Copyright © 2013 - 2024 Jaben Cargman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using MailKit.Net.Smtp; -using MailKit.Security; - -namespace Papercut.Rules.Domain.Relaying -{ - public static class RelayRuleExtensions - { - public static void PopulateServerFromUri(this RelayRule rule, string smtpServer) - { - if (rule == null) throw new ArgumentNullException(nameof(rule)); - - var uri = new Uri("smtp://" + smtpServer); - - rule.SmtpServer = uri.DnsSafeHost; - rule.SmtpPort = uri.IsDefaultPort ? 25 : uri.Port; - } - - public static async Task CreateConnectedSmtpClientAsync(this RelayRule forwardRule, CancellationToken token) - { - if (forwardRule == null) throw new ArgumentNullException(nameof(forwardRule)); - - var client = new SmtpClient(); - - await client.ConnectAsync( - forwardRule.SmtpServer, - forwardRule.SmtpPort, - forwardRule.SmtpUseSSL ? SecureSocketOptions.Auto : SecureSocketOptions.None, - token); - - // Note: since we don't have an OAuth2 token, disable - // the XOAUTH2 authentication mechanism. - client.AuthenticationMechanisms.Remove("XOAUTH2"); - - if (!string.IsNullOrWhiteSpace(forwardRule.SmtpPassword) - && !string.IsNullOrWhiteSpace(forwardRule.SmtpUsername)) - { - // Note: only needed if the SMTP server requires authentication - await client.AuthenticateAsync(forwardRule.SmtpUsername, forwardRule.SmtpPassword, token); - } - - return client; - } - } -} \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Rules/IRuleRepository.cs b/src/Papercut.Rules/Domain/Rules/IRuleRepository.cs index a46ee1a3..4cca035d 100644 --- a/src/Papercut.Rules/Domain/Rules/IRuleRepository.cs +++ b/src/Papercut.Rules/Domain/Rules/IRuleRepository.cs @@ -18,12 +18,11 @@ using Papercut.Core.Domain.Rules; -namespace Papercut.Rules.Domain.Rules +namespace Papercut.Rules.Domain.Rules; + +public interface IRuleRepository { - public interface IRuleRepository - { - void SaveRules(IList rules, string path); + void SaveRules(IList rules, string path); - IList LoadRules(string path); - } + IList LoadRules(string path); } \ No newline at end of file diff --git a/src/Papercut.Rules/Domain/Rules/RuleBase.cs b/src/Papercut.Rules/Domain/Rules/RuleBase.cs index 6719a06b..09c1de07 100644 --- a/src/Papercut.Rules/Domain/Rules/RuleBase.cs +++ b/src/Papercut.Rules/Domain/Rules/RuleBase.cs @@ -26,69 +26,66 @@ using Papercut.Common.Helper; using Papercut.Core.Domain.Rules; -namespace Papercut.Rules.Domain.Rules +namespace Papercut.Rules.Domain.Rules; + +[Serializable] +public abstract class RuleBase : IRule { - [Serializable] - public abstract class RuleBase : IRule - { - bool _isEnabled; + bool _isEnabled; - protected RuleBase() - { - this.Id = Guid.NewGuid(); - } + protected RuleBase() + { + this.Id = Guid.NewGuid(); + } - [Category("Information")] - public Guid Id { get; protected set; } + [Category("Information")] + public Guid Id { get; protected set; } - [Category("State")] - [Browsable(true)] - [DisplayName("Is Enabled")] - [Description("Is the Rule Enabled for Processing?")] - public virtual bool IsEnabled + [Category("State")] + [Browsable(true)] + [DisplayName("Is Enabled")] + [Description("Is the Rule Enabled for Processing?")] + public virtual bool IsEnabled + { + get => this._isEnabled; + set { - get => this._isEnabled; - set - { - if (value.Equals(this._isEnabled)) return; - this._isEnabled = value; - this.OnPropertyChanged(nameof(this.IsEnabled)); - } + if (value.Equals(this._isEnabled)) return; + this._isEnabled = value; + this.OnPropertyChanged(nameof(this.IsEnabled)); } + } - [Category("Information")] - [Browsable(false)] - public virtual string Type => this.GetType().Name; - - [Category("Information")] - [Browsable(false)] - [JsonIgnore] - public virtual string Description - => - this.GetPropertiesForDescription() - .Where(s => !s.Key.IsAny("Id", "Type", "Description")) - .OrderBy(s => s.Key) - .ToFormattedPairs() - .Join("\r\n"); + [Category("Information")] + [Browsable(false)] + public virtual string Type => this.GetType().Name; - public event PropertyChangedEventHandler? PropertyChanged; + [Category("Information")] + [Browsable(false)] + [JsonIgnore] + public virtual string Description + => + this.GetPropertiesForDescription() + .Where(s => !s.Key.IsAny("Id", "Type", "Description")) + .OrderBy(s => s.Key) + .ToFormattedPairs() + .Join("\r\n"); - public abstract void PopulateFromRule(MimeMessage message); + public event PropertyChangedEventHandler? PropertyChanged; - protected virtual IEnumerable>> GetPropertiesForDescription() - { - return this.GetProperties(); - } + protected virtual IEnumerable>> GetPropertiesForDescription() + { + return this.GetProperties(); + } - [NotifyPropertyChangedInvocator] - protected virtual void OnPropertyChanged(string propertyName) + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChangedEventHandler? handler = this.PropertyChanged; + if (handler != null) { - PropertyChangedEventHandler? handler = this.PropertyChanged; - if (handler != null) - { - handler(this, new PropertyChangedEventArgs(propertyName)); - handler(this, new PropertyChangedEventArgs("Description")); - } + handler(this, new PropertyChangedEventArgs(propertyName)); + handler(this, new PropertyChangedEventArgs(nameof(Description))); } } } \ No newline at end of file diff --git a/src/Papercut.Rules/Infrastructure/ObservableExtensions.cs b/src/Papercut.Rules/Infrastructure/ObservableExtensions.cs index 2fcbece5..f8a304aa 100644 --- a/src/Papercut.Rules/Infrastructure/ObservableExtensions.cs +++ b/src/Papercut.Rules/Infrastructure/ObservableExtensions.cs @@ -18,20 +18,19 @@ using System.Reactive.Linq; -namespace Papercut.Rules.Infrastructure +namespace Papercut.Rules.Infrastructure; + +public static class ObservableExtensions { - public static class ObservableExtensions + public static IObservable RetryWithDelay(this IObservable source, int retryCount, TimeSpan timeSpan) { - public static IObservable RetryWithDelay(this IObservable source, int retryCount, TimeSpan timeSpan) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - if (timeSpan < TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(timeSpan)); - if (timeSpan == TimeSpan.Zero) - return source.Retry(retryCount); + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (timeSpan < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(timeSpan)); + if (timeSpan == TimeSpan.Zero) + return source.Retry(retryCount); - return source.Catch(Observable.Timer(timeSpan).SelectMany(_ => source).Retry(retryCount)); - } + return source.Catch(Observable.Timer(timeSpan).SelectMany(_ => source).Retry(retryCount)); } } \ No newline at end of file diff --git a/src/Papercut.Rules/Infrastructure/RuleRepository.cs b/src/Papercut.Rules/Infrastructure/RuleRepository.cs index 2c6dd8c6..48ef64a4 100644 --- a/src/Papercut.Rules/Infrastructure/RuleRepository.cs +++ b/src/Papercut.Rules/Infrastructure/RuleRepository.cs @@ -29,130 +29,129 @@ using Papercut.Rules.Domain.Relaying; using Papercut.Rules.Domain.Rules; -namespace Papercut.Rules.Infrastructure +namespace Papercut.Rules.Infrastructure; + +public class RuleRepository : IRuleRepository { - public class RuleRepository : IRuleRepository + private readonly JsonSerializerSettings _serializationSettings; + + public RuleRepository() { - private readonly JsonSerializerSettings _serializationSettings; + this._serializationSettings = + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Include, + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + SerializationBinder = new NamespaceMigrationSerializationBinder(this.NamespaceMigrations) + }; + } - public RuleRepository() + IEnumerable NamespaceMigrations + { + get { - this._serializationSettings = - new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.Auto, - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Include, - TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, - SerializationBinder = new NamespaceMigrationSerializationBinder(this.NamespaceMigrations) - }; + var assembly = "Papercut.Rules"; + + yield return new NamespaceMigrationImpl( + assembly, + "Papercut.Rules.Implementations.ConditionalForwardWithRelayRule", + typeof(ConditionalForwardWithRetryRule)); + + yield return new NamespaceMigrationImpl( + assembly, + "Papercut.Rules.Implementations.ConditionalForwardRule", + typeof(ConditionalForwardRule)); + + yield return new NamespaceMigrationImpl( + assembly, + "Papercut.Rules.Implementations.RelayRule", + typeof(RelayRule)); + + yield return new NamespaceMigrationImpl( + assembly, + "Papercut.Rules.Implementations.ForwardRule", + typeof(ForwardRule)); } + } - IEnumerable NamespaceMigrations - { - get - { - var assembly = "Papercut.Rules"; - - yield return new NamespaceMigrationImpl( - assembly, - "Papercut.Rules.Implementations.ConditionalForwardWithRelayRule", - typeof(ConditionalForwardWithRetryRule)); - - yield return new NamespaceMigrationImpl( - assembly, - "Papercut.Rules.Implementations.ConditionalForwardRule", - typeof(ConditionalForwardRule)); - - yield return new NamespaceMigrationImpl( - assembly, - "Papercut.Rules.Implementations.RelayRule", - typeof(RelayRule)); - - yield return new NamespaceMigrationImpl( - assembly, - "Papercut.Rules.Implementations.ForwardRule", - typeof(ForwardRule)); - } - } + public void SaveRules([NotNull] IList rules, string path) + { + if (rules == null) throw new ArgumentNullException(nameof(rules)); + if (path == null) throw new ArgumentNullException(nameof(path)); - public void SaveRules([NotNull] IList rules, string path) - { - if (rules == null) throw new ArgumentNullException(nameof(rules)); - if (path == null) throw new ArgumentNullException(nameof(path)); + JsonHelpers.SaveJson(rules, path, setting: this._serializationSettings); + } - JsonHelpers.SaveJson(rules, path, setting: this._serializationSettings); - } + public IList LoadRules([NotNull] string path) + { + if (path == null) throw new ArgumentNullException(nameof(path)); - public IList LoadRules([NotNull] string path) - { - if (path == null) throw new ArgumentNullException(nameof(path)); + return JsonHelpers.LoadJson>( + path, + () => new List(0), + setting: this._serializationSettings); + } - return JsonHelpers.LoadJson>( - path, - () => new List(0), - setting: this._serializationSettings); - } + #region Begin Static Container Registrations - #region Begin Static Container Registrations + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register([NotNull] ContainerBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register([NotNull] ContainerBuilder builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + builder.RegisterType().As().InstancePerDependency(); + } - builder.RegisterType().As().InstancePerDependency(); - } + #endregion - #endregion + public interface INamespaceMigration + { + string FromAssembly { get; } - public interface INamespaceMigration - { - string FromAssembly { get; } + string FromType { get; } - string FromType { get; } + Type ToType { get; } + } - Type ToType { get; } + internal class NamespaceMigrationImpl : INamespaceMigration + { + public NamespaceMigrationImpl(string fromAssembly, string fromType, Type toType) + { + this.FromAssembly = fromAssembly; + this.FromType = fromType; + this.ToType = toType; } - internal class NamespaceMigrationImpl : INamespaceMigration - { - public NamespaceMigrationImpl(string fromAssembly, string fromType, Type toType) - { - this.FromAssembly = fromAssembly; - this.FromType = fromType; - this.ToType = toType; - } + public string FromAssembly { get; } - public string FromAssembly { get; } + public string FromType { get; } - public string FromType { get; } + public Type ToType { get; } + } - public Type ToType { get; } - } + public class NamespaceMigrationSerializationBinder : DefaultSerializationBinder + { + private readonly INamespaceMigration[] _migrations; - public class NamespaceMigrationSerializationBinder : DefaultSerializationBinder + public NamespaceMigrationSerializationBinder(IEnumerable migrations) { - private readonly INamespaceMigration[] _migrations; - - public NamespaceMigrationSerializationBinder(IEnumerable migrations) - { - this._migrations = migrations.IfNullEmpty().ToArray(); - } + this._migrations = migrations.IfNullEmpty().ToArray(); + } - public override Type BindToType(string assemblyName, string typeName) + public override Type BindToType(string assemblyName, string typeName) + { + var migration = this._migrations.SingleOrDefault(p => p.FromAssembly == assemblyName && p.FromType == typeName); + if(migration != null) { - var migration = this._migrations.SingleOrDefault(p => p.FromAssembly == assemblyName && p.FromType == typeName); - if(migration != null) - { - return migration.ToType; - } - return base.BindToType(assemblyName, typeName); + return migration.ToType; } + return base.BindToType(assemblyName, typeName); } } } \ No newline at end of file diff --git a/src/Papercut.Rules/Papercut.Rules.csproj b/src/Papercut.Rules/Papercut.Rules.csproj index 51a1b790..27684f72 100644 --- a/src/Papercut.Rules/Papercut.Rules.csproj +++ b/src/Papercut.Rules/Papercut.Rules.csproj @@ -15,18 +15,23 @@ - 8.0.0 + 8.1.1 - 4.4.0 + 4.8.0 - 4.4.0 + 4.8.0 13.0.3 - + + + + + + \ No newline at end of file diff --git a/src/Papercut.Rules/PapercutRuleModule.cs b/src/Papercut.Rules/PapercutRuleModule.cs index d8612a4c..c406fde5 100644 --- a/src/Papercut.Rules/PapercutRuleModule.cs +++ b/src/Papercut.Rules/PapercutRuleModule.cs @@ -20,13 +20,12 @@ using Papercut.Core.Infrastructure.Container; -namespace Papercut.Rules +namespace Papercut.Rules; + +public class PapercutRuleModule : Module { - public class PapercutRuleModule : Module + protected override void Load(ContainerBuilder builder) { - protected override void Load(ContainerBuilder builder) - { - builder.RegisterStaticMethods(this.ThisAssembly); - } + builder.RegisterStaticMethods(this.ThisAssembly); } } \ No newline at end of file diff --git a/src/Papercut.Service/Infrastructure/IPComm/PublishAppEventsHandlerToClientService.cs b/src/Papercut.Service/Infrastructure/IPComm/PublishAppEventsHandlerToClientService.cs index c9252b12..401cf4ea 100644 --- a/src/Papercut.Service/Infrastructure/IPComm/PublishAppEventsHandlerToClientService.cs +++ b/src/Papercut.Service/Infrastructure/IPComm/PublishAppEventsHandlerToClientService.cs @@ -15,62 +15,61 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Papercut.Service.Infrastructure.IPComm +namespace Papercut.Service.Infrastructure.IPComm; + +public class PublishAppEventsHandlerToClientService( + PapercutIPCommClientFactory ipCommClientFactory, + ILogger logger) : IEventHandler, + IEventHandler, + IEventHandler { - public class PublishAppEventsHandlerToClientService( - PapercutIPCommClientFactory ipCommClientFactory, - ILogger logger) : IEventHandler, - IEventHandler, - IEventHandler + public Task HandleAsync(PapercutServiceExitEvent @event, CancellationToken token = default) { - public Task HandleAsync(PapercutServiceExitEvent @event, CancellationToken token = default) - { - return this.PublishAsync(@event); - } + return this.PublishAsync(@event); + } - public Task HandleAsync(PapercutServicePreStartEvent @event, CancellationToken token = default) - { - return this.PublishAsync(@event); - } + public Task HandleAsync(PapercutServicePreStartEvent @event, CancellationToken token = default) + { + return this.PublishAsync(@event); + } - public Task HandleAsync(PapercutServiceReadyEvent @event, CancellationToken token = default) - { - return this.PublishAsync(@event); - } + public Task HandleAsync(PapercutServiceReadyEvent @event, CancellationToken token = default) + { + return this.PublishAsync(@event); + } - public async Task PublishAsync(T @event) - where T : IEvent - { - var ipCommClient = ipCommClientFactory.GetClient(PapercutIPCommClientConnectTo.UI); + public async Task PublishAsync(T @event) + where T : IEvent + { + var ipCommClient = ipCommClientFactory.GetClient(PapercutIPCommClientConnectTo.UI); - try - { - logger.Information( - $"Publishing {{@{@event.GetType().Name}}} to the Papercut Client", - @event); + try + { + logger.Information( + $"Publishing {{@{@event.GetType().Name}}} to the Papercut Client", + @event); - await ipCommClient.PublishEventServer(@event, TimeSpan.FromMilliseconds(500)); - } - catch (TaskCanceledException) - { - } - catch (Exception ex) - { - logger.Warning( - ex, - "Failed to publish {Endpoint} specified. Papercut UI is most likely not running.", - ipCommClient.Endpoint); - } + await ipCommClient.PublishEventServer(@event, TimeSpan.FromMilliseconds(500)); } - - #region Begin Static Container Registrations - - static void Register(ContainerBuilder builder) + catch (TaskCanceledException) { - builder.RegisterType().AsImplementedInterfaces().AsSelf() - .InstancePerLifetimeScope(); } + catch (Exception ex) + { + logger.Warning( + ex, + "Failed to publish {Endpoint} specified. Papercut UI is most likely not running.", + ipCommClient.Endpoint); + } + } + + #region Begin Static Container Registrations - #endregion + static void Register(ContainerBuilder builder) + { + builder.RegisterType().AsImplementedInterfaces().AsSelf() + .InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Service/Infrastructure/Logging/AddReadFromConfiguration.cs b/src/Papercut.Service/Infrastructure/Logging/AddReadFromConfiguration.cs new file mode 100644 index 00000000..378d5999 --- /dev/null +++ b/src/Papercut.Service/Infrastructure/Logging/AddReadFromConfiguration.cs @@ -0,0 +1,40 @@ +// Papercut +// +// Copyright © 2008 - 2012 Ken Robertson +// Copyright © 2013 - 2024 Jaben Cargman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using Microsoft.Extensions.Configuration; + +using Serilog.Configuration; + +namespace Papercut.Service.Infrastructure.Logging; + +public class AddReadFromConfiguration(IConfiguration configuration) : ILoggerSettings +{ + public void Configure(LoggerConfiguration loggerConfiguration) + { + loggerConfiguration.ReadFrom.Configuration(configuration); + } + + #region Begin Static Container Registrations + + static void Register(ContainerBuilder builder) + { + builder.RegisterType().As(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Papercut.Service/Infrastructure/Paths/ReplyWithDefaultMessageSavePathService.cs b/src/Papercut.Service/Infrastructure/Paths/ReplyWithDefaultMessageSavePathService.cs index e0a09536..bf6535f0 100644 --- a/src/Papercut.Service/Infrastructure/Paths/ReplyWithDefaultMessageSavePathService.cs +++ b/src/Papercut.Service/Infrastructure/Paths/ReplyWithDefaultMessageSavePathService.cs @@ -18,40 +18,30 @@ using Papercut.Core.Infrastructure.Network; -namespace Papercut.Service.Infrastructure.Paths +namespace Papercut.Service.Infrastructure.Paths; + +public class ReplyWithDefaultMessageSavePathService(MessagePathConfigurator messagePathConfigurator, SmtpServerOptions smtpServerOptions) + : IEventHandler { - public class ReplyWithDefaultMessageSavePathService : IEventHandler + public Task HandleAsync(AppProcessExchangeEvent @event, CancellationToken token = default) { - readonly MessagePathConfigurator _messagePathConfigurator; - - readonly SmtpServerOptions _smtpServerOptions; - - public ReplyWithDefaultMessageSavePathService(MessagePathConfigurator messagePathConfigurator, SmtpServerOptions smtpServerOptions) - { - this._messagePathConfigurator = messagePathConfigurator; - this._smtpServerOptions = smtpServerOptions; - } - - public Task HandleAsync(AppProcessExchangeEvent @event, CancellationToken token = default) - { - // respond with the current save path... - @event.MessageWritePath = this._messagePathConfigurator.DefaultSavePath; + // respond with the current save path... + @event.MessageWritePath = messagePathConfigurator.DefaultSavePath; - // share our current ip and port binding for the SMTP server. - @event.IP = this._smtpServerOptions.IP; - @event.Port = this._smtpServerOptions.Port; + // share our current ip and port binding for the SMTP server. + @event.IP = smtpServerOptions.IP; + @event.Port = smtpServerOptions.Port; - return Task.CompletedTask; - } - - #region Begin Static Container Registrations + return Task.CompletedTask; + } - static void Register(ContainerBuilder builder) - { - builder.RegisterType().AsImplementedInterfaces().AsSelf() - .InstancePerLifetimeScope(); - } + #region Begin Static Container Registrations - #endregion + static void Register(ContainerBuilder builder) + { + builder.RegisterType().AsImplementedInterfaces().AsSelf() + .InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Service/Papercut.Service.csproj b/src/Papercut.Service/Papercut.Service.csproj index 70b97359..f8f8b012 100644 --- a/src/Papercut.Service/Papercut.Service.csproj +++ b/src/Papercut.Service/Papercut.Service.csproj @@ -8,12 +8,16 @@ enable enable icons\Papercut-icon.ico + Linux + + Always + @@ -21,14 +25,14 @@ - + - - + + - - + + @@ -56,4 +60,8 @@ Always + + + + \ No newline at end of file diff --git a/src/Papercut.Service/PapercutServiceModule.cs b/src/Papercut.Service/PapercutServiceModule.cs index 763b76a7..022d9861 100644 --- a/src/Papercut.Service/PapercutServiceModule.cs +++ b/src/Papercut.Service/PapercutServiceModule.cs @@ -16,22 +16,14 @@ // limitations under the License. -using System.Reflection; - -using Autofac; - using AutofacSerilogIntegration; -using Papercut.Common.Helper; - namespace Papercut.Service; public class PapercutServiceModule : Module { protected override void Load(ContainerBuilder builder) { - builder.Register(_ => new ApplicationMeta("Papercut.Service", Assembly.GetExecutingAssembly().GetVersion())).As().SingleInstance(); - builder.RegisterLogger(); builder.RegisterStaticMethods(this.ThisAssembly); diff --git a/src/Papercut.Service/Program.cs b/src/Papercut.Service/Program.cs index 07e95a99..e91a92ed 100644 --- a/src/Papercut.Service/Program.cs +++ b/src/Papercut.Service/Program.cs @@ -16,31 +16,30 @@ // limitations under the License. +using System.Reflection; + using Autofac.Extensions.DependencyInjection; using ElectronNET.API; -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Papercut.Common.Helper; using Papercut.Core.Infrastructure.Logging; -using Serilog.Core; -using Serilog.Debugging; -using Serilog.ExceptionalLogContext; - namespace Papercut.Service; public class Program { - const string AppName= "Papercut.SMTP.Service"; + private static readonly IAppMeta AppMeta = new ApplicationMeta("Papercut.SMTP.Service", Assembly.GetExecutingAssembly().GetVersion()); private static readonly CancellationTokenSource _cancellationTokenSource = new(); public static async Task Main(string[] args) { - Console.Title = AppName; + Console.Title = AppMeta.AppName; Log.Logger = BootstrapLogger.CreateBootstrapLogger(args); @@ -51,7 +50,7 @@ public static async Task RunAsync(string[] args) { try { - await CreateHostBuilder(args).Build().RunAsync(_cancellationTokenSource.Token); + await CreateWebApp(args).RunAsync(_cancellationTokenSource.Token); } catch (Exception ex) when (ex is not TaskCanceledException and not ObjectDisposedException) { @@ -69,56 +68,49 @@ public static void Shutdown() _cancellationTokenSource.Cancel(); } - public static IHostBuilder CreateHostBuilder(string[] args) + private static WebApplication CreateWebApp(string[] args) { - return Host.CreateDefaultBuilder(args) - .UseWindowsService( - options => - { - options.ServiceName = AppName; - }) - .UseContentRoot(AppContext.BaseDirectory) - .UseServiceProviderFactory(new AutofacServiceProviderFactory()) - .ConfigureServices( - (context, sp) => - { - sp.AddSingleton(context.Configuration); - sp.AddSingleton(context.HostingEnvironment); - sp.AddSingleton(new LoggingLevelSwitch()); - }) - .ConfigureWebHostDefaults( - (webBuilder) => - { - webBuilder.ConfigureLogging( - s => - { - s.ClearProviders(); - }); - webBuilder.UseElectron(args); - webBuilder.UseStartup(); - }) - .UseSerilog(CreateDefaultLogger) - .ConfigureServices( - (context, sp) => - { - if (HybridSupport.IsElectronActive) - sp.AddHostedService(); - }); + var applicationOptions = new WebApplicationOptions() + { ContentRootPath = AppContext.BaseDirectory, Args = args }; + + var builder = WebApplication.CreateBuilder(applicationOptions); + + builder.Host.UseWindowsService( + options => + { + options.ServiceName = AppMeta.AppName; + }).ConfigureServices( + (context, sp) => + { + sp.AddSingleton(context.Configuration); + sp.AddSingleton(context.HostingEnvironment); + + if (HybridSupport.IsElectronActive) + sp.AddHostedService(); + }); + + builder.Logging.ClearProviders(); + builder.Host.UseSerilog(); + builder.WebHost.UseElectron(args); + + var startup = new PapercutServiceStartup(); + + builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory(startup.ConfigureContainer)); + startup.ConfigureServices(builder.Services); + + var webApp = builder.Build(); + + startup.Configure(webApp); + + return webApp; } - private static void CreateDefaultLogger(HostBuilderContext context, IServiceProvider services, LoggerConfiguration configuration) + #region Begin Static Container Registrations + + static void Register(ContainerBuilder builder) { - var appMeta = services.GetRequiredService(); - - configuration - .Enrich.FromLogContext() - .Enrich.WithExceptionalLogContext() - .Enrich.WithProperty("AppName", appMeta.AppName) - .Enrich.WithProperty("AppVersion", appMeta.AppVersion) - .WriteTo.Console() - .ReadFrom.Configuration(context.Configuration) - .ReadFrom.Services(services); - - SelfLog.Enable(s => Console.Error.WriteLine(s)); + builder.RegisterInstance(AppMeta).As().SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.Service/appsettings.Development.json b/src/Papercut.Service/appsettings.Development.json index bd749f97..3b944a2e 100644 --- a/src/Papercut.Service/appsettings.Development.json +++ b/src/Papercut.Service/appsettings.Development.json @@ -1,5 +1,6 @@ { "Serilog": { + "Using": [ "Serilog.Sinks.Seq" ], "WriteTo": [ { "Name": "Seq", @@ -7,12 +8,14 @@ } ], "MinimumLevel": { - "Default": "Verbose", + "Default": "Debug", "Override": { "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Information" } } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + + "Urls": "http://localhost:37408" } \ No newline at end of file diff --git a/src/Papercut.Service/appsettings.Production.json b/src/Papercut.Service/appsettings.Production.json new file mode 100644 index 00000000..8593c62d --- /dev/null +++ b/src/Papercut.Service/appsettings.Production.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/src/Papercut.Service/appsettings.json b/src/Papercut.Service/appsettings.json index 4deccdc2..578e131d 100644 --- a/src/Papercut.Service/appsettings.json +++ b/src/Papercut.Service/appsettings.json @@ -16,7 +16,5 @@ "LoggingPath": "%DataDirectory%\\Logs;%BaseDirectory%\\Logs" }, - "AllowedHosts": "*", - - "Urls": "http://localhost:37408" + "AllowedHosts": "*" } \ No newline at end of file diff --git a/src/Papercut.Service/package-lock.json b/src/Papercut.Service/package-lock.json new file mode 100644 index 00000000..96fe13b3 --- /dev/null +++ b/src/Papercut.Service/package-lock.json @@ -0,0 +1,2113 @@ +{ + "name": "Papercut", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "Papercut", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "detect-port": "^1.2.1", + "electron": "^29.1.1", + "electron-localshortcut": "^3.1.0", + "socket.io": "4.7.4", + "sudo-prompt": "^9.2.1", + "tmp": "^0.2.1" + }, + "devDependencies": { + "eslint": "^8.57.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "optional": true + }, + "node_modules/detect-port": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "dependencies": { + "address": "^1.0.1", + "debug": "4" + }, + "bin": { + "detect": "bin/detect-port.js", + "detect-port": "bin/detect-port.js" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron": { + "version": "29.4.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.2.tgz", + "integrity": "sha512-XyIkuWQguwY8hGtLg0j5Q4Fqphdbh0ctBsKCSVzJ/R7Z2+2WN/oQ1M+zYwchmfiDgiuL3EKkrBrfPdxXYdMr+A==", + "hasInstallScript": true, + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-is-accelerator": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz", + "integrity": "sha512-fLGSAjXZtdn1sbtZxx52+krefmtNuVwnJCV2gNiVt735/ARUboMl8jnNC9fZEqQdlAv2ZrETfmBUsoQci5evJA==" + }, + "node_modules/electron-localshortcut": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/electron-localshortcut/-/electron-localshortcut-3.2.1.tgz", + "integrity": "sha512-DWvhKv36GsdXKnaFFhEiK8kZZA+24/yFLgtTwJJHc7AFgDjNRIBJZ/jq62Y/dWv9E4ypYwrVWN2bVrCYw1uv7Q==", + "dependencies": { + "debug": "^4.0.1", + "electron-is-accelerator": "^0.1.0", + "keyboardevent-from-electron-accelerator": "^2.0.0", + "keyboardevents-areequal": "^0.2.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "optional": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "optional": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "optional": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "optional": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "optional": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyboardevent-from-electron-accelerator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-2.0.0.tgz", + "integrity": "sha512-iQcmNA0M4ETMNi0kG/q0h/43wZk7rMeKYrXP7sqKIJbHkTU8Koowgzv+ieR/vWJbOwxx5nDC3UnudZ0aLSu4VA==" + }, + "node_modules/keyboardevents-areequal": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz", + "integrity": "sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", + "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "optional": true + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/Papercut.UI/AppLayer/AppCommands/AppCommandHub.cs b/src/Papercut.UI/AppLayer/AppCommands/AppCommandHub.cs index c6bd6ccf..72e3908f 100644 --- a/src/Papercut.UI/AppLayer/AppCommands/AppCommandHub.cs +++ b/src/Papercut.UI/AppLayer/AppCommands/AppCommandHub.cs @@ -23,42 +23,41 @@ using Papercut.Domain.AppCommands; -namespace Papercut.AppLayer.AppCommands +namespace Papercut.AppLayer.AppCommands; + +public class AppCommandHub : Disposable, IAppCommandHub { - public class AppCommandHub : Disposable, IAppCommandHub - { - private readonly Subject _onShutdown = new Subject(); + private readonly Subject _onShutdown = new Subject(); - public IObservable OnShutdown => this._onShutdown; + public IObservable OnShutdown => this._onShutdown; - public void Shutdown(int exitCode = 0) - { - var command = new ShutdownCommand(exitCode); - this._onShutdown.OnNext(command); - } + public void Shutdown(int exitCode = 0) + { + var command = new ShutdownCommand(exitCode); + this._onShutdown.OnNext(command); + } - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - this._onShutdown.Dispose(); - } + this._onShutdown.Dispose(); } + } - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().As().SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().As().SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/AppCommands/ShutdownCommandHandler.cs b/src/Papercut.UI/AppLayer/AppCommands/ShutdownCommandHandler.cs index 0f139f26..ef7d2d66 100644 --- a/src/Papercut.UI/AppLayer/AppCommands/ShutdownCommandHandler.cs +++ b/src/Papercut.UI/AppLayer/AppCommands/ShutdownCommandHandler.cs @@ -29,73 +29,72 @@ using Papercut.Core.Infrastructure.Lifecycle; using Papercut.Domain.AppCommands; -namespace Papercut.AppLayer.AppCommands +namespace Papercut.AppLayer.AppCommands; + +public class ShutdownCommandHandler : Disposable, IStartable { - public class ShutdownCommandHandler : Disposable, IStartable - { - private readonly IAppCommandHub _appCommandHub; + private readonly IAppCommandHub _appCommandHub; - private readonly IAppMeta _appMeta; + private readonly IAppMeta _appMeta; - private readonly ILogger _logger; + private readonly ILogger _logger; - private readonly IMessageBus _messageBus; + private readonly IMessageBus _messageBus; - private IDisposable _shutdownObservable; + private IDisposable _shutdownObservable; - public ShutdownCommandHandler(IAppCommandHub appCommandHub, IMessageBus messageBus, IAppMeta appMeta, ILogger logger) - { - this._appCommandHub = appCommandHub; - this._messageBus = messageBus; - this._appMeta = appMeta; - this._logger = logger.ForContext(); - } + public ShutdownCommandHandler(IAppCommandHub appCommandHub, IMessageBus messageBus, IAppMeta appMeta, ILogger logger) + { + this._appCommandHub = appCommandHub; + this._messageBus = messageBus; + this._appMeta = appMeta; + this._logger = logger.ForContext(); + } - public void Start() - { - this.InitObservables(); - } + public void Start() + { + this.InitObservables(); + } - private void InitObservables() - { - this._shutdownObservable = this._appCommandHub.OnShutdown - .ObserveOn(Dispatcher.CurrentDispatcher) - .SubscribeAsync( - async @event => - { - this._logger.Information("Shutdown Executed {ExitCode}", @event.ExitCode); + private void InitObservables() + { + this._shutdownObservable = this._appCommandHub.OnShutdown + .ObserveOn(Dispatcher.CurrentDispatcher) + .SubscribeAsync( + async @event => + { + this._logger.Information("Shutdown Executed {ExitCode}", @event.ExitCode); - // fire shutdown event - await this._messageBus.PublishAsync( - new PapercutClientExitEvent() { AppMeta = this._appMeta }); + // fire shutdown event + await this._messageBus.PublishAsync( + new PapercutClientExitEvent() { AppMeta = this._appMeta }); - Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; - Application.Current.Shutdown(@event.ExitCode); - }); - } + Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; + Application.Current.Shutdown(@event.ExitCode); + }); + } - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - this._shutdownObservable?.Dispose(); - } + this._shutdownObservable?.Dispose(); } + } - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces().SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces().SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Behaviors/DragDropIFile.cs b/src/Papercut.UI/AppLayer/Behaviors/DragDropIFile.cs index 981b2bd1..7f061ebc 100644 --- a/src/Papercut.UI/AppLayer/Behaviors/DragDropIFile.cs +++ b/src/Papercut.UI/AppLayer/Behaviors/DragDropIFile.cs @@ -26,57 +26,56 @@ using Papercut.Core.Domain.Message; using Papercut.Helpers; -namespace Papercut.AppLayer.Behaviors +namespace Papercut.AppLayer.Behaviors; + +public class DragDropIFile : Behavior { - public class DragDropIFile : Behavior + Point? _dragStartPoint; + + protected override void OnAttached() { - Point? _dragStartPoint; + base.OnAttached(); - protected override void OnAttached() - { - base.OnAttached(); + // bind it! + this.AssociatedObject.PreviewMouseMove += this.ListBoxPreviewMouseMove; + this.AssociatedObject.PreviewMouseLeftButtonDown += this.ListBoxPreviewLeftMouseDown; + this.AssociatedObject.PreviewMouseUp += this.ListBoxPreviewMouseUp; + } - // bind it! - this.AssociatedObject.PreviewMouseMove += this.ListBoxPreviewMouseMove; - this.AssociatedObject.PreviewMouseLeftButtonDown += this.ListBoxPreviewLeftMouseDown; - this.AssociatedObject.PreviewMouseUp += this.ListBoxPreviewMouseUp; - } + void ListBoxPreviewLeftMouseDown(object sender, MouseButtonEventArgs e) + { + if (sender is not ListBox parent) return; - void ListBoxPreviewLeftMouseDown(object sender, MouseButtonEventArgs e) - { - if (sender is not ListBox parent) return; + if (this._dragStartPoint == null) this._dragStartPoint = e.GetPosition(parent); + } - if (this._dragStartPoint == null) this._dragStartPoint = e.GetPosition(parent); - } + void ListBoxPreviewMouseMove(object sender, MouseEventArgs e) + { + if (sender is not ListBox parent || this._dragStartPoint == null) return; - void ListBoxPreviewMouseMove(object sender, MouseEventArgs e) - { - if (sender is not ListBox parent || this._dragStartPoint == null) return; + if (((DependencyObject)e.OriginalSource).FindAncestor() != null) return; - if (((DependencyObject)e.OriginalSource).FindAncestor() != null) return; + Point dragPoint = e.GetPosition(parent); - Point dragPoint = e.GetPosition(parent); + Vector potentialDragLength = dragPoint - this._dragStartPoint.Value; - Vector potentialDragLength = dragPoint - this._dragStartPoint.Value; + if (potentialDragLength.Length > 10) + { + // Get the object source for the selected item - if (potentialDragLength.Length > 10) + // If the data is not null then start the drag drop operation + if (parent.GetObjectDataFromPoint(this._dragStartPoint.Value) is IFile entry && !string.IsNullOrWhiteSpace(entry.File)) { - // Get the object source for the selected item - - // If the data is not null then start the drag drop operation - if (parent.GetObjectDataFromPoint(this._dragStartPoint.Value) is IFile entry && !string.IsNullOrWhiteSpace(entry.File)) - { - var dataObject = new DataObject(DataFormats.FileDrop, new[] { entry.File }); - DragDrop.DoDragDrop(parent, dataObject, DragDropEffects.Copy); - } - - this._dragStartPoint = null; + var dataObject = new DataObject(DataFormats.FileDrop, new[] { entry.File }); + DragDrop.DoDragDrop(parent, dataObject, DragDropEffects.Copy); } - } - void ListBoxPreviewMouseUp(object sender, MouseButtonEventArgs e) - { this._dragStartPoint = null; } } + + void ListBoxPreviewMouseUp(object sender, MouseButtonEventArgs e) + { + this._dragStartPoint = null; + } } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurBase.cs b/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurBase.cs index 299420bb..36205da4 100644 --- a/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurBase.cs +++ b/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurBase.cs @@ -21,28 +21,27 @@ using Microsoft.Xaml.Behaviors; -namespace Papercut.AppLayer.Behaviors +namespace Papercut.AppLayer.Behaviors; + +public class InteractivityBlurBase : Behavior + where T : DependencyObject { - public class InteractivityBlurBase : Behavior - where T : DependencyObject - { - // Using a DependencyProperty as the backing store for BlurRadius. This enables animation, styling, binding, etc... - public static readonly DependencyProperty BlurRadiusProperty = - DependencyProperty.Register( - $"BlurRadius_{typeof(T).Name}", - typeof(int), - typeof(FrameworkElement), - new UIPropertyMetadata(0)); + // Using a DependencyProperty as the backing store for BlurRadius. This enables animation, styling, binding, etc... + public static readonly DependencyProperty BlurRadiusProperty = + DependencyProperty.Register( + $"BlurRadius_{typeof(T).Name}", + typeof(int), + typeof(FrameworkElement), + new UIPropertyMetadata(0)); - public int BlurRadius - { - get => (int) this.GetValue(BlurRadiusProperty); - set => this.SetValue(BlurRadiusProperty, value); - } + public int BlurRadius + { + get => (int) this.GetValue(BlurRadiusProperty); + set => this.SetValue(BlurRadiusProperty, value); + } - protected virtual BlurEffect GetBlurEffect() - { - return new BlurEffect { Radius = this.BlurRadius }; - } + protected virtual BlurEffect GetBlurEffect() + { + return new BlurEffect { Radius = this.BlurRadius }; } } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurOnDisabled.cs b/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurOnDisabled.cs index e15c9895..34f386c6 100644 --- a/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurOnDisabled.cs +++ b/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurOnDisabled.cs @@ -18,21 +18,20 @@ using System.Windows; -namespace Papercut.AppLayer.Behaviors +namespace Papercut.AppLayer.Behaviors; + +public class InteractivityBlurOnDisabled : InteractivityBlurBase { - public class InteractivityBlurOnDisabled : InteractivityBlurBase + protected override void OnAttached() { - protected override void OnAttached() - { - base.OnAttached(); - this.AssociatedObject.IsEnabledChanged += this.AssociatedObjectIsEnabledChanged; - } + base.OnAttached(); + this.AssociatedObject.IsEnabledChanged += this.AssociatedObjectIsEnabledChanged; + } - void AssociatedObjectIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) - { - this.AssociatedObject.Effect = this.AssociatedObject.IsEnabled - ? null - : this.GetBlurEffect(); - } + void AssociatedObjectIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + this.AssociatedObject.Effect = this.AssociatedObject.IsEnabled + ? null + : this.GetBlurEffect(); } } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurWindowOnDeactivate.cs b/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurWindowOnDeactivate.cs index 4cde2aa4..b85d4793 100644 --- a/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurWindowOnDeactivate.cs +++ b/src/Papercut.UI/AppLayer/Behaviors/InteractivityBlurWindowOnDeactivate.cs @@ -18,32 +18,31 @@ using System.Windows; -namespace Papercut.AppLayer.Behaviors +namespace Papercut.AppLayer.Behaviors; + +public class InteractivityBlurWindowOnDeactivate : InteractivityBlurBase { - public class InteractivityBlurWindowOnDeactivate : InteractivityBlurBase + protected override void OnAttached() + { + base.OnAttached(); + this.AssociatedObject.Deactivated += this.AssociatedObjectOnDeactivated; + this.AssociatedObject.Activated += this.AssociatedObjectOnActivated; + } + + void AssociatedObjectOnActivated(object sender, EventArgs eventArgs) + { + this.ToggleBlurEffect(false); + } + + void AssociatedObjectOnDeactivated(object sender, EventArgs eventArgs) + { + this.ToggleBlurEffect(true); + } + + protected void ToggleBlurEffect(bool enable) { - protected override void OnAttached() - { - base.OnAttached(); - this.AssociatedObject.Deactivated += this.AssociatedObjectOnDeactivated; - this.AssociatedObject.Activated += this.AssociatedObjectOnActivated; - } - - void AssociatedObjectOnActivated(object sender, EventArgs eventArgs) - { - this.ToggleBlurEffect(false); - } - - void AssociatedObjectOnDeactivated(object sender, EventArgs eventArgs) - { - this.ToggleBlurEffect(true); - } - - protected void ToggleBlurEffect(bool enable) - { - this.AssociatedObject.Effect = enable ? this.GetBlurEffect() : null; - } + this.AssociatedObject.Effect = enable ? this.GetBlurEffect() : null; } } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Behaviors/SettingBindingExtension.cs b/src/Papercut.UI/AppLayer/Behaviors/SettingBindingExtension.cs index c7f4e6e4..f2b1dde3 100644 --- a/src/Papercut.UI/AppLayer/Behaviors/SettingBindingExtension.cs +++ b/src/Papercut.UI/AppLayer/Behaviors/SettingBindingExtension.cs @@ -18,29 +18,28 @@ using System.Windows.Data; -namespace Papercut.AppLayer.Behaviors +namespace Papercut.AppLayer.Behaviors; + +/// +/// Very useful code from here: +/// http://tomlev2.wordpress.com/2008/11/18/wpf-binding-to-application-settings-using-a-markup-extension/ +/// +public class SettingBindingExtension : Binding { - /// - /// Very useful code from here: - /// http://tomlev2.wordpress.com/2008/11/18/wpf-binding-to-application-settings-using-a-markup-extension/ - /// - public class SettingBindingExtension : Binding + public SettingBindingExtension() { - public SettingBindingExtension() - { - this.Initialize(); - } + this.Initialize(); + } - public SettingBindingExtension(string path) - : base(path) - { - this.Initialize(); - } + public SettingBindingExtension(string path) + : base(path) + { + this.Initialize(); + } - void Initialize() - { - this.Source = Properties.Settings.Default; - this.Mode = BindingMode.TwoWay; - } + void Initialize() + { + this.Source = Properties.Settings.Default; + this.Mode = BindingMode.TwoWay; } } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Cleanup/TempDirectoryCleanupService.cs b/src/Papercut.UI/AppLayer/Cleanup/TempDirectoryCleanupService.cs index 0aca5a8e..8f210503 100644 --- a/src/Papercut.UI/AppLayer/Cleanup/TempDirectoryCleanupService.cs +++ b/src/Papercut.UI/AppLayer/Cleanup/TempDirectoryCleanupService.cs @@ -23,68 +23,67 @@ using Papercut.Core.Domain.Application; using Papercut.Domain.LifecycleHooks; -namespace Papercut.AppLayer.Cleanup +namespace Papercut.AppLayer.Cleanup; + +public class TempDirectoryCleanupService(IAppMeta appMeta, ILogger logger) : IAppLifecyclePreExit { - public class TempDirectoryCleanupService(IAppMeta appMeta, ILogger logger) : IAppLifecyclePreExit + public Task OnPreExit() { - public Task OnPreExit() - { - // time for temp file cleanup - this.TryCleanUpTempDirectories(); + // time for temp file cleanup + this.TryCleanUpTempDirectories(); - return Task.FromResult(AppLifecycleActionResultType.Continue); - } + return Task.FromResult(AppLifecycleActionResultType.Continue); + } + + private void TryCleanUpTempDirectories() + { + int deleteCount = 0; + string tempPath = Path.GetTempPath(); - private void TryCleanUpTempDirectories() + // try cleanup... + try { - int deleteCount = 0; - string tempPath = Path.GetTempPath(); + string[] tmpDirs = Directory.GetDirectories(tempPath, $"{appMeta.AppName}-*"); - // try cleanup... - try + foreach (string tmpDir in tmpDirs) { - string[] tmpDirs = Directory.GetDirectories(tempPath, $"{appMeta.AppName}-*"); - - foreach (string tmpDir in tmpDirs) + try { - try - { - Directory.Delete(tmpDir, true); - deleteCount++; - } - catch (Exception ex) - { - logger.Warning(ex, @"Unable to delete {TempDirectory}", tmpDir); - } + Directory.Delete(tmpDir, true); + deleteCount++; + } + catch (Exception ex) + { + logger.Warning(ex, @"Unable to delete {TempDirectory}", tmpDir); } } - catch (Exception ex) - { - logger.Warning( - ex, - @"Failure running temp directory cleanup on temp path {TempPath}", - tempPath); - } - - if (deleteCount > 0) - logger.Information("Deleted {DeleteCount} temporary directories", deleteCount); + } + catch (Exception ex) + { + logger.Warning( + ex, + @"Failure running temp directory cleanup on temp path {TempPath}", + tempPath); } - #region Begin Static Container Registrations + if (deleteCount > 0) + logger.Information("Deleted {DeleteCount} temporary directories", deleteCount); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces() - .SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces() + .SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Diagnostics/ReportVersionService.cs b/src/Papercut.UI/AppLayer/Diagnostics/ReportVersionService.cs index 1e177b7e..d36178c6 100644 --- a/src/Papercut.UI/AppLayer/Diagnostics/ReportVersionService.cs +++ b/src/Papercut.UI/AppLayer/Diagnostics/ReportVersionService.cs @@ -21,31 +21,30 @@ using Autofac; -namespace Papercut.AppLayer.Diagnostics +namespace Papercut.AppLayer.Diagnostics; + +public class ReportVersionService(ILogger logger) : IStartable { - public class ReportVersionService(ILogger logger) : IStartable + public void Start() { - public void Start() - { - var productVersion = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; - - logger.Information("Papercut Version {PapercutVersion:l}", productVersion); - } + var productVersion = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; - #region Begin Static Container Registrations + logger.Information("Papercut Version {PapercutVersion:l}", productVersion); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces().SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces().SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Events/EventPublishAll.cs b/src/Papercut.UI/AppLayer/Events/EventPublishAll.cs index accfc4ac..9f3ca10c 100644 --- a/src/Papercut.UI/AppLayer/Events/EventPublishAll.cs +++ b/src/Papercut.UI/AppLayer/Events/EventPublishAll.cs @@ -23,47 +23,46 @@ using Papercut.Common.Domain; using Papercut.Core.Infrastructure.MessageBus; -namespace Papercut.AppLayer.Events -{ - public class EventPublishAll : AutofacMessageBus - { - private readonly IEventAggregator _eventAggregator; +namespace Papercut.AppLayer.Events; - public EventPublishAll( - ILifetimeScope scope, - IEventAggregator eventAggregator) - : base(scope) - { - this._eventAggregator = eventAggregator; - } +public class EventPublishAll : AutofacMessageBus +{ + private readonly IEventAggregator _eventAggregator; - public override async Task PublishAsync(T eventObject, CancellationToken token) - { - if (eventObject == null) throw new ArgumentNullException(nameof(eventObject)); + public EventPublishAll( + ILifetimeScope scope, + IEventAggregator eventAggregator) + : base(scope) + { + this._eventAggregator = eventAggregator; + } - await base.PublishAsync(eventObject, token); - await this._eventAggregator.PublishOnUIThreadAsync(eventObject, token); - } + public override async Task PublishAsync(T eventObject, CancellationToken token) + { + if (eventObject == null) throw new ArgumentNullException(nameof(eventObject)); - protected override async Task HandleAsync(T eventObject, IEventHandler @event, CancellationToken token) - { - await Execute.OnUIThreadAsync(async () => await base.HandleAsync(eventObject, @event, token)); - } + await base.PublishAsync(eventObject, token); + await this._eventAggregator.PublishOnUIThreadAsync(eventObject, token); + } - #region Begin Static Container Registrations + protected override async Task HandleAsync(T eventObject, IEventHandler @event, CancellationToken token) + { + await Execute.OnUIThreadAsync(async () => await base.HandleAsync(eventObject, @event, token)); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().As().InstancePerLifetimeScope(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().As().InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/HtmlPreviews/HtmlPreviewGeneratorImpl.cs b/src/Papercut.UI/AppLayer/HtmlPreviews/HtmlPreviewGeneratorImpl.cs index c6489475..17219b30 100644 --- a/src/Papercut.UI/AppLayer/HtmlPreviews/HtmlPreviewGeneratorImpl.cs +++ b/src/Papercut.UI/AppLayer/HtmlPreviews/HtmlPreviewGeneratorImpl.cs @@ -27,76 +27,75 @@ using Papercut.Domain.HtmlPreviews; using Papercut.Helpers; -namespace Papercut.AppLayer.HtmlPreviews +namespace Papercut.AppLayer.HtmlPreviews; + +public class HtmlPreviewGeneratorImpl : IHtmlPreviewGenerator { - public class HtmlPreviewGeneratorImpl : IHtmlPreviewGenerator - { - readonly IAppMeta _appMeta; + readonly IAppMeta _appMeta; - readonly ILogger _logger; + readonly ILogger _logger; - public HtmlPreviewGeneratorImpl(ILogger logger, IAppMeta appMeta) - { - this._logger = logger; - this._appMeta = appMeta; - } + public HtmlPreviewGeneratorImpl(ILogger logger, IAppMeta appMeta) + { + this._logger = logger; + this._appMeta = appMeta; + } - public string GetHtmlPreview(MimeMessage? mailMessageEx, string? tempDir = null) - { - ArgumentNullException.ThrowIfNull(mailMessageEx); + public string GetHtmlPreview(MimeMessage? mailMessageEx, string? tempDir = null) + { + ArgumentNullException.ThrowIfNull(mailMessageEx); - tempDir = tempDir ?? this.CreateUniqueTempDirectory(); - var visitor = new HtmlPreviewVisitor(tempDir); - mailMessageEx.Accept(visitor); + tempDir = tempDir ?? this.CreateUniqueTempDirectory(); + var visitor = new HtmlPreviewVisitor(tempDir); + mailMessageEx.Accept(visitor); - return visitor.HtmlBody; - } + return visitor.HtmlBody; + } - public string? GetHtmlPreviewFile(MimeMessage? mailMessageEx, string? tempDir = null) - { - tempDir = tempDir ?? this.CreateUniqueTempDirectory(); + public string? GetHtmlPreviewFile(MimeMessage? mailMessageEx, string? tempDir = null) + { + tempDir = tempDir ?? this.CreateUniqueTempDirectory(); - var htmlPreview = this.GetHtmlPreview(mailMessageEx, tempDir); + var htmlPreview = this.GetHtmlPreview(mailMessageEx, tempDir); - string? htmlFile = Path.Combine(tempDir, "index.html"); + string? htmlFile = Path.Combine(tempDir, "index.html"); - this._logger.Verbose("Writing HTML Preview file {HtmlFile}", htmlFile); + this._logger.Verbose("Writing HTML Preview file {HtmlFile}", htmlFile); - File.WriteAllText(htmlFile, htmlPreview, Encoding.Unicode); + File.WriteAllText(htmlFile, htmlPreview, Encoding.Unicode); - return htmlFile; - } + return htmlFile; + } - string CreateUniqueTempDirectory() + string CreateUniqueTempDirectory() + { + string tempDir; + do { - string tempDir; - do - { - // find unique temp directory - tempDir = Path.Combine(Path.GetTempPath(), $"{this._appMeta.AppName}-{Guid.NewGuid().ToString().Truncate(6)}"); - } - while (Directory.Exists(tempDir)); - - Directory.CreateDirectory(tempDir); - - return tempDir; + // find unique temp directory + tempDir = Path.Combine(Path.GetTempPath(), $"{this._appMeta.AppName}-{Guid.NewGuid().ToString().Truncate(6)}"); } + while (Directory.Exists(tempDir)); - #region Begin Static Container Registrations + Directory.CreateDirectory(tempDir); - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + return tempDir; + } - builder.RegisterType().AsImplementedInterfaces() - .InstancePerLifetimeScope(); - } + #region Begin Static Container Registrations - #endregion + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.RegisterType().AsImplementedInterfaces() + .InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/IpComm/BackendServiceCoordinator.cs b/src/Papercut.UI/AppLayer/IpComm/BackendServiceCoordinator.cs index 47657feb..f889e67f 100644 --- a/src/Papercut.UI/AppLayer/IpComm/BackendServiceCoordinator.cs +++ b/src/Papercut.UI/AppLayer/IpComm/BackendServiceCoordinator.cs @@ -34,227 +34,226 @@ using Papercut.Infrastructure.IPComm; using Papercut.Infrastructure.IPComm.Network; -namespace Papercut.AppLayer.IpComm +namespace Papercut.AppLayer.IpComm; + +public class BackendServiceCoordinator : IBackendServiceStatus, IAppLifecycleStarted, + IEventHandler, + IEventHandler, + IEventHandler, + IEventHandler, + IEventHandler, IOrderable { - public class BackendServiceCoordinator : IBackendServiceStatus, IAppLifecycleStarted, - IEventHandler, - IEventHandler, - IEventHandler, - IEventHandler, - IEventHandler, IOrderable - { - const string BackendServiceFailureMessage = - "Papercut Backend Service Exception Attempting to Contact"; - - readonly PapercutIPCommClientFactory _ipCommClientFactory; + const string BackendServiceFailureMessage = + "Papercut Backend Service Exception Attempting to Contact"; - readonly ILogger _logger; + readonly PapercutIPCommClientFactory _ipCommClientFactory; - readonly IMessageBus _messageBus; + readonly ILogger _logger; - private bool? _isOnline = null; + readonly IMessageBus _messageBus; - Action _nextUpdateEvent; + private bool? _isOnline = null; - public BackendServiceCoordinator( - ILogger logger, - IMessageBus messageBus, - PapercutIPCommClientFactory ipCommClientFactory) - { - this._logger = logger; - this._messageBus = messageBus; - this._ipCommClientFactory = ipCommClientFactory; - - IObservable rulesUpdateObservable = Observable - .Create( - o => - { - this._nextUpdateEvent = o.OnNext; - return Disposable.Empty; - }).SubscribeOn(TaskPoolScheduler.Default); - - // flush rules every 10 seconds - rulesUpdateObservable.Buffer(TimeSpan.FromSeconds(10)) - .Where(e => e.Any()) - .SubscribeAsync(async events => await this.PublishUpdateEvent(events.Last())); - } + Action _nextUpdateEvent; - public async Task OnStartedAsync() - { - await this.AttemptExchangeAsync(); - } + public BackendServiceCoordinator( + ILogger logger, + IMessageBus messageBus, + PapercutIPCommClientFactory ipCommClientFactory) + { + this._logger = logger; + this._messageBus = messageBus; + this._ipCommClientFactory = ipCommClientFactory; - public bool IsOnline - { - get => this._isOnline ?? false; - private set => this._isOnline = value; - } + IObservable rulesUpdateObservable = Observable + .Create( + o => + { + this._nextUpdateEvent = o.OnNext; + return Disposable.Empty; + }).SubscribeOn(TaskPoolScheduler.Default); + + // flush rules every 10 seconds + rulesUpdateObservable.Buffer(TimeSpan.FromSeconds(10)) + .Where(e => e.Any()) + .SubscribeAsync(async events => await this.PublishUpdateEvent(events.Last())); + } - public async Task HandleAsync(PapercutServiceExitEvent @event, CancellationToken token) - { - await this.SetOnlineStatus(false, token); - } + public async Task OnStartedAsync() + { + await this.AttemptExchangeAsync(); + } - public async Task HandleAsync(PapercutServicePreStartEvent @event, CancellationToken token) - { - await this.SetOnlineStatus(true, token); - } + public bool IsOnline + { + get => this._isOnline ?? false; + private set => this._isOnline = value; + } - public async Task HandleAsync(PapercutServiceReadyEvent @event, CancellationToken token) - { - await this.SetOnlineStatus(true, token); - } + public async Task HandleAsync(PapercutServiceExitEvent @event, CancellationToken token) + { + await this.SetOnlineStatus(false, token); + } - public Task HandleAsync(RulesUpdatedEvent @event, CancellationToken token) - { - if (this.IsOnline) - { - this._nextUpdateEvent(@event); - } + public async Task HandleAsync(PapercutServicePreStartEvent @event, CancellationToken token) + { + await this.SetOnlineStatus(true, token); + } - return Task.CompletedTask; - } + public async Task HandleAsync(PapercutServiceReadyEvent @event, CancellationToken token) + { + await this.SetOnlineStatus(true, token); + } - public async Task HandleAsync(SettingsUpdatedEvent @event, CancellationToken token) + public Task HandleAsync(RulesUpdatedEvent @event, CancellationToken token) + { + if (this.IsOnline) { - await this.PublishSmtpUpdatedAsync(@event, token); + this._nextUpdateEvent(@event); } - public int Order => 10; + return Task.CompletedTask; + } + + public async Task HandleAsync(SettingsUpdatedEvent @event, CancellationToken token) + { + await this.PublishSmtpUpdatedAsync(@event, token); + } + + public int Order => 10; - private async Task SetOnlineStatus(bool newStatus, CancellationToken token = default) + private async Task SetOnlineStatus(bool newStatus, CancellationToken token = default) + { + if (this._isOnline != newStatus) { - if (this._isOnline != newStatus) - { - this.IsOnline = newStatus; - - await this._messageBus.PublishAsync( - new PapercutServiceStatusEvent( - this.IsOnline - ? PapercutServiceStatusType.Online - : PapercutServiceStatusType.Offline), - token); - } + this.IsOnline = newStatus; + + await this._messageBus.PublishAsync( + new PapercutServiceStatusEvent( + this.IsOnline + ? PapercutServiceStatusType.Online + : PapercutServiceStatusType.Offline), + token); } + } - public async Task PublishSmtpUpdatedAsync(SettingsUpdatedEvent @event, CancellationToken token) - { - if (!this.IsOnline) return; + public async Task PublishSmtpUpdatedAsync(SettingsUpdatedEvent @event, CancellationToken token) + { + if (!this.IsOnline) return; - // check if the setting changed - if (@event.PreviousSettings.IP == @event.NewSettings.IP && @event.PreviousSettings.Port == @event.NewSettings.Port) return; + // check if the setting changed + if (@event.PreviousSettings.IP == @event.NewSettings.IP && @event.PreviousSettings.Port == @event.NewSettings.Port) return; - try - { - var messenger = this.GetClient(); + try + { + var messenger = this.GetClient(); - // update the backend service with the new ip/port settings... - var smtpServerBindEvent = new SmtpServerBindEvent( - Properties.Settings.Default.IP, - Properties.Settings.Default.Port); + // update the backend service with the new ip/port settings... + var smtpServerBindEvent = new SmtpServerBindEvent( + Properties.Settings.Default.IP, + Properties.Settings.Default.Port); - bool successfulPublish = await messenger.PublishEventServer( - smtpServerBindEvent, - TimeSpan.FromSeconds(1)); + bool successfulPublish = await messenger.PublishEventServer( + smtpServerBindEvent, + TimeSpan.FromSeconds(1)); - this._logger.Information( - successfulPublish - ? "Successfully pushed new Smtp Server Binding to Backend Service" - : "Papercut Backend Service Failed to Update. Could be offline."); - } - catch (Exception ex) when (ex is TaskCanceledException || ex is ObjectDisposedException) - { - // do nothing - } - catch (Exception ex) - { - this._logger.Warning(ex, BackendServiceFailureMessage); - } + this._logger.Information( + successfulPublish + ? "Successfully pushed new Smtp Server Binding to Backend Service" + : "Papercut Backend Service Failed to Update. Could be offline."); } + catch (Exception ex) when (ex is TaskCanceledException || ex is ObjectDisposedException) + { + // do nothing + } + catch (Exception ex) + { + this._logger.Warning(ex, BackendServiceFailureMessage); + } + } - private async Task AttemptExchangeAsync(CancellationToken token = default) + private async Task AttemptExchangeAsync(CancellationToken token = default) + { + try { - try - { - var sendEvent = new AppProcessExchangeEvent(); + var sendEvent = new AppProcessExchangeEvent(); - // attempt to connect to the backend server... - var ipCommClient = this.GetClient(); + // attempt to connect to the backend server... + var ipCommClient = this.GetClient(); - var receivedEvent = await ipCommClient.ExchangeEventServer(sendEvent, TimeSpan.FromSeconds(1)); + var receivedEvent = await ipCommClient.ExchangeEventServer(sendEvent, TimeSpan.FromSeconds(1)); - if (receivedEvent != null) + if (receivedEvent != null) + { + if (!string.IsNullOrWhiteSpace(receivedEvent.MessageWritePath)) { - if (!string.IsNullOrWhiteSpace(receivedEvent.MessageWritePath)) - { - this._logger.Debug( - "Background Process Returned {@Event} -- Publishing", - receivedEvent); + this._logger.Debug( + "Background Process Returned {@Event} -- Publishing", + receivedEvent); - await this._messageBus.PublishAsync(receivedEvent, token); - } + await this._messageBus.PublishAsync(receivedEvent, token); + } - await this.SetOnlineStatus(true, token); + await this.SetOnlineStatus(true, token); - return; - } + return; } - catch (Exception ex) when (ex is TaskCanceledException or ObjectDisposedException) - { - // do nothing - } - catch (Exception ex) - { - this._logger.Warning(ex, BackendServiceFailureMessage); - } - - // publish status message regardless - await this.SetOnlineStatus(false, token); + } + catch (Exception ex) when (ex is TaskCanceledException or ObjectDisposedException) + { + // do nothing + } + catch (Exception ex) + { + this._logger.Warning(ex, BackendServiceFailureMessage); } - async Task PublishUpdateEvent(RulesUpdatedEvent @event) + // publish status message regardless + await this.SetOnlineStatus(false, token); + } + + async Task PublishUpdateEvent(RulesUpdatedEvent @event) + { + try { - try - { - var ipCommClient = this.GetClient(); + var ipCommClient = this.GetClient(); - bool successfulPublish = await ipCommClient.PublishEventServer(@event, TimeSpan.FromSeconds(1)); + bool successfulPublish = await ipCommClient.PublishEventServer(@event, TimeSpan.FromSeconds(1)); - this._logger.Information( - successfulPublish - ? "Successfully Updated Rules on Backend Service" - : "Papercut Backend Service Failed to Update Rules. Could be offline."); - } - catch (Exception ex) when (ex is TaskCanceledException || ex is ObjectDisposedException) - { - // do nothing - } - catch (Exception ex) - { - this._logger.Warning(ex, BackendServiceFailureMessage); - } + this._logger.Information( + successfulPublish + ? "Successfully Updated Rules on Backend Service" + : "Papercut Backend Service Failed to Update Rules. Could be offline."); } - - PapercutIPCommClient GetClient() + catch (Exception ex) when (ex is TaskCanceledException || ex is ObjectDisposedException) + { + // do nothing + } + catch (Exception ex) { - return this._ipCommClientFactory.GetClient(PapercutIPCommClientConnectTo.Service); + this._logger.Warning(ex, BackendServiceFailureMessage); } + } - #region Begin Static Container Registrations + PapercutIPCommClient GetClient() + { + return this._ipCommClientFactory.GetClient(PapercutIPCommClientConnectTo.Service); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces() - .InstancePerLifetimeScope(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces() + .InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/IpComm/PapercutIpCommManager.cs b/src/Papercut.UI/AppLayer/IpComm/PapercutIpCommManager.cs index 1b08366a..404c6abf 100644 --- a/src/Papercut.UI/AppLayer/IpComm/PapercutIpCommManager.cs +++ b/src/Papercut.UI/AppLayer/IpComm/PapercutIpCommManager.cs @@ -22,67 +22,66 @@ using Papercut.Domain.LifecycleHooks; using Papercut.Infrastructure.IPComm.Network; -namespace Papercut.AppLayer.IpComm +namespace Papercut.AppLayer.IpComm; + +public class PapercutIpCommManager : Disposable, IAppLifecycleStarted { - public class PapercutIpCommManager : Disposable, IAppLifecycleStarted - { - readonly ILogger _logger; + readonly ILogger _logger; + + private readonly PapercutIPCommEndpoints _papercutIpCommEndpoints; - private readonly PapercutIPCommEndpoints _papercutIpCommEndpoints; + readonly PapercutIPCommServer _papercutIpCommServer; + + public PapercutIpCommManager( + PapercutIPCommEndpoints papercutIpCommEndpoints, + PapercutIPCommServer ipCommServer, + ILogger logger) + { + this._papercutIpCommEndpoints = papercutIpCommEndpoints; + this._logger = logger; + this._papercutIpCommServer = ipCommServer; + } - readonly PapercutIPCommServer _papercutIpCommServer; + public async Task OnStartedAsync() + { + await this._papercutIpCommServer.StopAsync(); - public PapercutIpCommManager( - PapercutIPCommEndpoints papercutIpCommEndpoints, - PapercutIPCommServer ipCommServer, - ILogger logger) + try { - this._papercutIpCommEndpoints = papercutIpCommEndpoints; - this._logger = logger; - this._papercutIpCommServer = ipCommServer; + await this._papercutIpCommServer.StartAsync(this._papercutIpCommEndpoints.UI); } - - public async Task OnStartedAsync() + catch (Exception ex) { - await this._papercutIpCommServer.StopAsync(); - - try - { - await this._papercutIpCommServer.StartAsync(this._papercutIpCommEndpoints.UI); - } - catch (Exception ex) - { - this._logger.Warning( - ex, - "Papercut IPComm Server failed to bind to the {Address} {Port} specified. The port may already be in use by another process.", - this._papercutIpCommServer.ListenIpAddress, - this._papercutIpCommServer.ListenPort); - } + this._logger.Warning( + ex, + "Papercut IPComm Server failed to bind to the {Address} {Port} specified. The port may already be in use by another process.", + this._papercutIpCommServer.ListenIpAddress, + this._papercutIpCommServer.ListenPort); } + } - protected override async ValueTask DisposeAsync(bool disposing) + protected override async ValueTask DisposeAsync(bool disposing) + { + if (disposing) { - if (disposing) - { - await this._papercutIpCommServer.StopAsync(); - } + await this._papercutIpCommServer.StopAsync(); } + } - #region Begin Static Container Registrations + #region Begin Static Container Registrations - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - builder.RegisterType().AsImplementedInterfaces() - .SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces() + .SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Logging/UiLogSinkQueue.cs b/src/Papercut.UI/AppLayer/Logging/UiLogSinkQueue.cs index f71f5413..a8c83cf7 100644 --- a/src/Papercut.UI/AppLayer/Logging/UiLogSinkQueue.cs +++ b/src/Papercut.UI/AppLayer/Logging/UiLogSinkQueue.cs @@ -25,62 +25,61 @@ using Serilog.Configuration; using Serilog.Events; -namespace Papercut.AppLayer.LogSinks +namespace Papercut.AppLayer.LogSinks; + +public class UiLogSinkQueue : ILoggerSettings { - public class UiLogSinkQueue : ILoggerSettings - { - static readonly ConcurrentQueue _logQueue = new ConcurrentQueue(); + static readonly ConcurrentQueue _logQueue = new ConcurrentQueue(); - public void Configure(LoggerConfiguration loggerConfiguration) - { - bool showDebug = false; + public void Configure(LoggerConfiguration loggerConfiguration) + { + bool showDebug = false; #if DEBUG - showDebug = true; + showDebug = true; #endif - loggerConfiguration.WriteTo.Observers( - b => b.ObserveOn(TaskPoolScheduler.Default).Subscribe(le => - { - _logQueue.Enqueue(le); - this.OnLogEvent(); - }), - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - showDebug ? LogEventLevel.Debug : LogEventLevel.Information); - } - - public LogEvent? GetLastEvent() - { - return _logQueue.TryDequeue(out var log) ? log : null; - } - - public IEnumerable GetLastEvents() - { - while (this.GetLastEvent() is { } logEvent) + loggerConfiguration.WriteTo.Observers( + b => b.ObserveOn(TaskPoolScheduler.Default).Subscribe(le => { - yield return logEvent; - } - } + _logQueue.Enqueue(le); + this.OnLogEvent(); + }), + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + showDebug ? LogEventLevel.Debug : LogEventLevel.Information); + } - public event EventHandler LogEvent; + public LogEvent? GetLastEvent() + { + return _logQueue.TryDequeue(out var log) ? log : null; + } - protected virtual void OnLogEvent() + public IEnumerable GetLastEvents() + { + while (this.GetLastEvent() is { } logEvent) { - this.LogEvent?.Invoke(this, EventArgs.Empty); + yield return logEvent; } + } - #region Begin Static Container Registrations + public event EventHandler LogEvent; - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + protected virtual void OnLogEvent() + { + this.LogEvent?.Invoke(this, EventArgs.Empty); + } - builder.RegisterType().As().AsSelf(); - } + #region Begin Static Container Registrations - #endregion + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.RegisterType().As().AsSelf(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/NewVersionCheck/NewVersionCheckHandler.cs b/src/Papercut.UI/AppLayer/NewVersionCheck/NewVersionCheckHandler.cs index 1b2e6fa3..712e3b0c 100644 --- a/src/Papercut.UI/AppLayer/NewVersionCheck/NewVersionCheckHandler.cs +++ b/src/Papercut.UI/AppLayer/NewVersionCheck/NewVersionCheckHandler.cs @@ -19,8 +19,6 @@ using Autofac; using Autofac.Util; -using Microsoft.Extensions.Logging; - using NuGet.Versioning; using Papercut.Core.Infrastructure.Container; @@ -28,9 +26,11 @@ using Velopack; +using ILogger = Serilog.ILogger; + namespace Papercut.AppLayer.NewVersionCheck; -public class NewVersionCheckHandler(ILogger logger) : Disposable, IAppLifecycleStarted, INewVersionProvider +public class NewVersionCheckHandler(UpdateManager updateManager, ILogger logger) : Disposable, IAppLifecycleStarted, INewVersionProvider { private readonly CancellationTokenSource _cancellationTokenSource = new(); @@ -40,27 +40,27 @@ public class NewVersionCheckHandler(ILogger logger) : Di public Task OnStartedAsync() { - this._backgroundTask = Task.Run(this.RunNewVersionCheck, this._cancellationTokenSource.Token); + _backgroundTask = Task.Run(RunNewVersionCheck, _cancellationTokenSource.Token); - return this._backgroundTask.IsCompleted ? this._backgroundTask : Task.CompletedTask; + return _backgroundTask.IsCompleted ? _backgroundTask : Task.CompletedTask; } public async Task GetLatestVersionAsync(CancellationToken token = default) { - return await this._updateTask.Task; + return await _updateTask.Task; } protected override async ValueTask DisposeAsync(bool disposing) { if (disposing) { - if (this._backgroundTask != null) + if (_backgroundTask != null) { - await this._cancellationTokenSource.CancelAsync(); + await _cancellationTokenSource.CancelAsync(); try { - await this._backgroundTask; + await _backgroundTask; } catch (Exception) { @@ -68,11 +68,11 @@ protected override async ValueTask DisposeAsync(bool disposing) } } - this._updateTask.SetCanceled(); + _updateTask.SetCanceled(); try { - await this._updateTask.Task; + await _updateTask.Task; } catch (Exception) { @@ -83,35 +83,33 @@ protected override async ValueTask DisposeAsync(bool disposing) private async Task RunNewVersionCheck() { - var mgr = new UpdateManager("https://github.com/ChangemakerStudios/Papercut-SMTP", logger: logger); - - if (mgr.IsInstalled) + if (updateManager.IsInstalled) { try { // check for new version - var newVersion = await mgr.CheckForUpdatesAsync(); + var newVersion = await updateManager.CheckForUpdatesAsync(); if (newVersion != null) { - logger.LogInformation("New Version of Papercut SMTP is Available {@NewVersion}", newVersion); + logger.Information("New Version of Papercut SMTP is Available {@NewVersion}", newVersion); - this._updateTask.SetResult(newVersion); + _updateTask.SetResult(newVersion); } } catch (Exception ex) { - this._updateTask.SetException(ex); + _updateTask.SetException(ex); } } else { - logger.LogDebug("Papercut was not installed via Velopack. Cannot check for new versions."); + logger.Debug("Papercut was not installed via Velopack. Cannot check for new versions."); } this._updateTask.SetResult(null); // for testing - //this._updateTask.SetResult(new UpdateInfo(new VelopackAsset() { Version = new SemanticVersion(10, 0, 0) }, false)); + //_updateTask.SetResult(new UpdateInfo(new VelopackAsset() { Version = new SemanticVersion(10, 0, 0) }, false)); } #region Begin Static Container Registrations diff --git a/src/Papercut.UI/AppLayer/Notifications/NotificationMenuCoordinator.cs b/src/Papercut.UI/AppLayer/Notifications/NotificationMenuCoordinator.cs index 3ddd5050..0d2a3922 100644 --- a/src/Papercut.UI/AppLayer/Notifications/NotificationMenuCoordinator.cs +++ b/src/Papercut.UI/AppLayer/Notifications/NotificationMenuCoordinator.cs @@ -32,133 +32,132 @@ using Papercut.Domain.UiCommands; using Papercut.Infrastructure.Resources; -namespace Papercut.AppLayer.Notifications +namespace Papercut.AppLayer.Notifications; + +[UsedImplicitly] +public class NotificationMenuCoordinator : Disposable, IAppLifecyclePreExit, IEventHandler { - [UsedImplicitly] - public class NotificationMenuCoordinator : Disposable, IAppLifecyclePreExit, IEventHandler - { - private readonly IAppCommandHub _appCommandHub; + private readonly IAppCommandHub _appCommandHub; - readonly AppResourceLocator _resourceLocator; + readonly AppResourceLocator _resourceLocator; - private readonly IUiCommandHub _uiCommandHub; + private readonly IUiCommandHub _uiCommandHub; - NotifyIcon? _notification; + NotifyIcon? _notification; - public NotificationMenuCoordinator( - IAppCommandHub appCommandHub, - IUiCommandHub uiCommandHub, - AppResourceLocator resourceLocator) - { - this._appCommandHub = appCommandHub; - this._uiCommandHub = uiCommandHub; - this._resourceLocator = resourceLocator; + public NotificationMenuCoordinator( + IAppCommandHub appCommandHub, + IUiCommandHub uiCommandHub, + AppResourceLocator resourceLocator) + { + this._appCommandHub = appCommandHub; + this._uiCommandHub = uiCommandHub; + this._resourceLocator = resourceLocator; - this.InitObservables(); - } + this.InitObservables(); + } - public Task OnPreExit() - { - this.Reset(); + public Task OnPreExit() + { + this.Reset(); - return Task.FromResult(AppLifecycleActionResultType.Continue); - } + return Task.FromResult(AppLifecycleActionResultType.Continue); + } - public Task HandleAsync(PapercutClientReadyEvent @event, CancellationToken token) - { - if (this._notification == null) this.SetupNotification(); + public Task HandleAsync(PapercutClientReadyEvent @event, CancellationToken token) + { + if (this._notification == null) this.SetupNotification(); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - void InitObservables() - { - this._uiCommandHub.OnShowBalloonTip - .Sample(TimeSpan.FromSeconds(1), TaskPoolScheduler.Default) - .ObserveOn(TaskPoolScheduler.Default) - .Subscribe( - @event => - { - this._notification?.ShowBalloonTip( - @event.Timeout, - @event.TipTitle, - @event.TipText, - @event.ToolTipIcon); - }); - } + void InitObservables() + { + this._uiCommandHub.OnShowBalloonTip + .Sample(TimeSpan.FromSeconds(1), TaskPoolScheduler.Default) + .ObserveOn(TaskPoolScheduler.Default) + .Subscribe( + @event => + { + this._notification?.ShowBalloonTip( + @event.Timeout, + @event.TipTitle, + @event.TipText, + @event.ToolTipIcon); + }); + } - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - this.Reset(); - } + this.Reset(); } + } - private void Reset() - { - this._notification?.Dispose(); - this._notification = null; - } + private void Reset() + { + this._notification?.Dispose(); + this._notification = null; + } - void SetupNotification() + void SetupNotification() + { + // Set up the notification icon + this._notification = new NotifyIcon { - // Set up the notification icon - this._notification = new NotifyIcon - { - Icon = new Icon(this._resourceLocator.GetResource("App.ico").Stream), - Text = AppConstants.ApplicationName, - Visible = true - }; - - this._notification.Click += Notification_OnClick; - this._notification.BalloonTipClicked += - (_, _) => - { - this._uiCommandHub.ShowMainWindow(true); - }; - - this._notification.ContextMenuStrip = new ContextMenuStrip(); - - this._notification.ContextMenuStrip.Items.Add("Show", null, (_, _) => - { - this._uiCommandHub.ShowMainWindow(); - }); - this._notification.ContextMenuStrip.Items.Add("Options", null, (_, _) => + Icon = new Icon(this._resourceLocator.GetResource("App.ico").Stream), + Text = AppConstants.ApplicationName, + Visible = true + }; + + this._notification.Click += Notification_OnClick; + this._notification.BalloonTipClicked += + (_, _) => { - this._uiCommandHub.ShowOptionWindow(); - }); - this._notification.ContextMenuStrip.Items.Add("Exit", null, (_, _) => - { - this._appCommandHub.Shutdown(); - }); - } + this._uiCommandHub.ShowMainWindow(true); + }; + + this._notification.ContextMenuStrip = new ContextMenuStrip(); - private void Notification_OnClick(object? sender, EventArgs e) + this._notification.ContextMenuStrip.Items.Add("Show", null, (_, _) => { - if (e is MouseEventArgs { Button: MouseButtons.Left }) - { - this._uiCommandHub.ShowMainWindow(); - } + this._uiCommandHub.ShowMainWindow(); + }); + this._notification.ContextMenuStrip.Items.Add("Options", null, (_, _) => + { + this._uiCommandHub.ShowOptionWindow(); + }); + this._notification.ContextMenuStrip.Items.Add("Exit", null, (_, _) => + { + this._appCommandHub.Shutdown(); + }); + } - this._notification?.ContextMenuStrip?.Show(); + private void Notification_OnClick(object? sender, EventArgs e) + { + if (e is MouseEventArgs { Button: MouseButtons.Left }) + { + this._uiCommandHub.ShowMainWindow(); } - #region Begin Static Container Registrations + this._notification?.ContextMenuStrip?.Show(); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces() - .SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces() + .SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Settings/AppRunOnStartupService.cs b/src/Papercut.UI/AppLayer/Settings/AppRunOnStartupService.cs index 277d15d8..c1aff45d 100644 --- a/src/Papercut.UI/AppLayer/Settings/AppRunOnStartupService.cs +++ b/src/Papercut.UI/AppLayer/Settings/AppRunOnStartupService.cs @@ -28,82 +28,81 @@ using Papercut.Domain.Events; using Papercut.Domain.UiCommands; -namespace Papercut.AppLayer.Settings +namespace Papercut.AppLayer.Settings; + +public class AppRunOnStartupService(ILogger logger, IUiCommandHub uiCommandHub) : IEventHandler { - public class AppRunOnStartupService(ILogger logger, IUiCommandHub uiCommandHub) : IEventHandler + const string AppStartupKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; + + public Task HandleAsync(SettingsUpdatedEvent @event, CancellationToken token) { - const string AppStartupKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; + // check if the setting changed + if (@event.PreviousSettings.RunOnStartup == @event.NewSettings.RunOnStartup) + return Task.CompletedTask; - public Task HandleAsync(SettingsUpdatedEvent @event, CancellationToken token) + try { - // check if the setting changed - if (@event.PreviousSettings.RunOnStartup == @event.NewSettings.RunOnStartup) + var registryKey = Registry.CurrentUser.OpenSubKey(AppStartupKey, true); + + if (registryKey == null) + { + logger.Error("Failure opening registry key {AppStartupKey}", AppStartupKey); return Task.CompletedTask; + } + + var applicationName = PapercutAppConstants.Name; + var executablePath = PapercutAppConstants.ExecutablePath; - try + // is key currently set to this app executable? + bool runOnStartup = registryKey.GetValue(applicationName, null) + .ToType() == executablePath; + + if (Properties.Settings.Default.RunOnStartup && !runOnStartup) { - var registryKey = Registry.CurrentUser.OpenSubKey(AppStartupKey, true); - - if (registryKey == null) - { - logger.Error("Failure opening registry key {AppStartupKey}", AppStartupKey); - return Task.CompletedTask; - } - - var applicationName = PapercutAppConstants.Name; - var executablePath = PapercutAppConstants.ExecutablePath; - - // is key currently set to this app executable? - bool runOnStartup = registryKey.GetValue(applicationName, null) - .ToType() == executablePath; - - if (Properties.Settings.Default.RunOnStartup && !runOnStartup) - { - // turn on... - logger.Information( - "Setting AppStartup Registry {Key} to Run Papercut at {ExecutablePath}", - $"{AppStartupKey}\\{applicationName}", - executablePath); - - registryKey.SetValue(applicationName, executablePath); - } - else if (!Properties.Settings.Default.RunOnStartup && runOnStartup) - { - // turn off... - logger.Information( - "Attempting to Delete AppStartup Registry {Key}", - $"{AppStartupKey}\\{applicationName}"); - - registryKey.DeleteValue(applicationName, false); - } + // turn on... + logger.Information( + "Setting AppStartup Registry {Key} to Run Papercut at {ExecutablePath}", + $"{AppStartupKey}\\{applicationName}", + executablePath); + + registryKey.SetValue(applicationName, executablePath); } - catch (SecurityException ex) + else if (!Properties.Settings.Default.RunOnStartup && runOnStartup) { - logger.Error(ex, "Error Opening Registry for App Startup Service"); + // turn off... + logger.Information( + "Attempting to Delete AppStartup Registry {Key}", + $"{AppStartupKey}\\{applicationName}"); - uiCommandHub.ShowMessage( - "Failed to set Papercut to load at startup due to lack of permission. To fix, exit and run Papercut again with elevated (Admin) permissions.", - "Failed"); + registryKey.DeleteValue(applicationName, false); } + } + catch (SecurityException ex) + { + logger.Error(ex, "Error Opening Registry for App Startup Service"); - return Task.CompletedTask; + uiCommandHub.ShowMessage( + "Failed to set Papercut to load at startup due to lack of permission. To fix, exit and run Papercut again with elevated (Admin) permissions.", + "Failed"); } - #region Begin Static Container Registrations + return Task.CompletedTask; + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces() - .InstancePerLifetimeScope(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces() + .InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Settings/MergeServerBackendSettings.cs b/src/Papercut.UI/AppLayer/Settings/MergeServerBackendSettings.cs index 40a23bca..c10eecae 100644 --- a/src/Papercut.UI/AppLayer/Settings/MergeServerBackendSettings.cs +++ b/src/Papercut.UI/AppLayer/Settings/MergeServerBackendSettings.cs @@ -24,60 +24,59 @@ using Papercut.Core.Infrastructure.Network; using Papercut.Domain.Events; -namespace Papercut.AppLayer.Settings -{ - public class MergeServerBackendSettings : IEventHandler - { - readonly MessagePathConfigurator _configurator; +namespace Papercut.AppLayer.Settings; - readonly IMessageBus _messageBus; +public class MergeServerBackendSettings : IEventHandler +{ + readonly MessagePathConfigurator _configurator; - public MergeServerBackendSettings(MessagePathConfigurator configurator, IMessageBus messageBus) - { - this._configurator = configurator; - this._messageBus = messageBus; - } + readonly IMessageBus _messageBus; - public async Task HandleAsync(AppProcessExchangeEvent @event, CancellationToken token) - { - ArgumentNullException.ThrowIfNull(@event); + public MergeServerBackendSettings(MessagePathConfigurator configurator, IMessageBus messageBus) + { + this._configurator = configurator; + this._messageBus = messageBus; + } - if (string.IsNullOrWhiteSpace(@event.MessageWritePath)) - return; + public async Task HandleAsync(AppProcessExchangeEvent @event, CancellationToken token) + { + ArgumentNullException.ThrowIfNull(@event); - if (!this._configurator.LoadPaths.Any(s => s.StartsWith(@event.MessageWritePath, StringComparison.OrdinalIgnoreCase))) - { - // add it for watching... - Properties.Settings.Default.MessagePaths = $"{Properties.Settings.Default.MessagePaths};{@event.MessageWritePath}"; - } + if (string.IsNullOrWhiteSpace(@event.MessageWritePath)) + return; - var previousSettings = new Properties.Settings(); + if (!this._configurator.LoadPaths.Any(s => s.StartsWith(@event.MessageWritePath, StringComparison.OrdinalIgnoreCase))) + { + // add it for watching... + Properties.Settings.Default.MessagePaths = $"{Properties.Settings.Default.MessagePaths};{@event.MessageWritePath}"; + } - Properties.Settings.Default.CopyTo(previousSettings); + var previousSettings = new Properties.Settings(); - // save ip:port bindings as our own to keep in sync... - Properties.Settings.Default.IP = @event.IP; - Properties.Settings.Default.Port = @event.Port; - Properties.Settings.Default.Save(); + Properties.Settings.Default.CopyTo(previousSettings); - await this._messageBus.PublishAsync(new SettingsUpdatedEvent(previousSettings), token); - } + // save ip:port bindings as our own to keep in sync... + Properties.Settings.Default.IP = @event.IP; + Properties.Settings.Default.Port = @event.Port; + Properties.Settings.Default.Save(); - #region Begin Static Container Registrations + await this._messageBus.PublishAsync(new SettingsUpdatedEvent(previousSettings), token); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces() - .InstancePerLifetimeScope(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces() + .InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Settings/SaveSettingsOnExitService.cs b/src/Papercut.UI/AppLayer/Settings/SaveSettingsOnExitService.cs index 81098682..5079ba2e 100644 --- a/src/Papercut.UI/AppLayer/Settings/SaveSettingsOnExitService.cs +++ b/src/Papercut.UI/AppLayer/Settings/SaveSettingsOnExitService.cs @@ -20,56 +20,55 @@ using Papercut.Domain.LifecycleHooks; -namespace Papercut.AppLayer.Settings +namespace Papercut.AppLayer.Settings; + +public class SaveSettingsOnExitService : IAppLifecyclePreExit { - public class SaveSettingsOnExitService : IAppLifecyclePreExit - { - readonly ILogger _logger; + readonly ILogger _logger; - public SaveSettingsOnExitService(ILogger logger) - { - this._logger = logger; - } + public SaveSettingsOnExitService(ILogger logger) + { + this._logger = logger; + } - public Task OnPreExit() + public Task OnPreExit() + { + try { - try + if (Properties.Settings.Default.MainWindowHeight < 300) { - if (Properties.Settings.Default.MainWindowHeight < 300) - { - Properties.Settings.Default.MainWindowHeight = 300; - } - if (Properties.Settings.Default.MainWindowWidth < 400) - { - Properties.Settings.Default.MainWindowWidth = 400; - } - - this._logger.Debug("Saving Updated Settings..."); - Properties.Settings.Default.Save(); + Properties.Settings.Default.MainWindowHeight = 300; } - catch (Exception ex) + if (Properties.Settings.Default.MainWindowWidth < 400) { - this._logger.Error(ex, "Failure Saving Settings File"); + Properties.Settings.Default.MainWindowWidth = 400; } - return Task.FromResult(AppLifecycleActionResultType.Continue); + this._logger.Debug("Saving Updated Settings..."); + Properties.Settings.Default.Save(); + } + catch (Exception ex) + { + this._logger.Error(ex, "Failure Saving Settings File"); } - #region Begin Static Container Registrations + return Task.FromResult(AppLifecycleActionResultType.Continue); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces() - .SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces() + .SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/SmtpServers/SmtpServerCoordinator.cs b/src/Papercut.UI/AppLayer/SmtpServers/SmtpServerCoordinator.cs index 9f3e9295..f3c027b8 100644 --- a/src/Papercut.UI/AppLayer/SmtpServers/SmtpServerCoordinator.cs +++ b/src/Papercut.UI/AppLayer/SmtpServers/SmtpServerCoordinator.cs @@ -25,112 +25,111 @@ using Papercut.Domain.Events; using Papercut.Infrastructure.Smtp; -namespace Papercut.AppLayer.SmtpServers +namespace Papercut.AppLayer.SmtpServers; + +public class SmtpServerCoordinator : Disposable, + IEventHandler, IEventHandler { - public class SmtpServerCoordinator : Disposable, - IEventHandler, IEventHandler - { - readonly ILogger _logger; + readonly ILogger _logger; - readonly IMessageBus _messageBus; + readonly IMessageBus _messageBus; - private readonly PapercutSmtpServer _smtpServer; + private readonly PapercutSmtpServer _smtpServer; - public SmtpServerCoordinator( - PapercutSmtpServer smtpServer, - ILogger logger, - IMessageBus messageBus) - { - this._smtpServer = smtpServer; - this._logger = logger; - this._messageBus = messageBus; - } + public SmtpServerCoordinator( + PapercutSmtpServer smtpServer, + ILogger logger, + IMessageBus messageBus) + { + this._smtpServer = smtpServer; + this._logger = logger; + this._messageBus = messageBus; + } - public bool IsServerActive => this._smtpServer.IsActive; + public bool IsServerActive => this._smtpServer.IsActive; - public async Task HandleAsync(PapercutServiceStatusEvent @event, CancellationToken token = default) + public async Task HandleAsync(PapercutServiceStatusEvent @event, CancellationToken token = default) + { + if (@event.PapercutServiceStatus == PapercutServiceStatusType.Online) { - if (@event.PapercutServiceStatus == PapercutServiceStatusType.Online) - { - this._logger.Information( - "Papercut Backend Service is running. SMTP disabled in UI."); + this._logger.Information( + "Papercut Backend Service is running. SMTP disabled in UI."); - if (this.IsServerActive) await this.StopServerAsync(); - } - else if (@event.PapercutServiceStatus == PapercutServiceStatusType.Offline) - { - this._logger.Information( - "Papercut Backend Service is not running. SMTP enabled in UI."); + if (this.IsServerActive) await this.StopServerAsync(); + } + else if (@event.PapercutServiceStatus == PapercutServiceStatusType.Offline) + { + this._logger.Information( + "Papercut Backend Service is not running. SMTP enabled in UI."); - // give it a half second though - await Task.Delay(TimeSpan.FromMilliseconds(500), token); + // give it a half second though + await Task.Delay(TimeSpan.FromMilliseconds(500), token); - if (!this.IsServerActive) await this.ListenServerAsync(); - } + if (!this.IsServerActive) await this.ListenServerAsync(); } + } - public async Task HandleAsync(SettingsUpdatedEvent @event, CancellationToken token) - { - if (!this.IsServerActive) return; + public async Task HandleAsync(SettingsUpdatedEvent @event, CancellationToken token) + { + if (!this.IsServerActive) return; - if (@event.PreviousSettings.IP == @event.NewSettings.IP && @event.PreviousSettings.Port == @event.NewSettings.Port) return; + if (@event.PreviousSettings.IP == @event.NewSettings.IP && @event.PreviousSettings.Port == @event.NewSettings.Port) return; - await this.ListenServerAsync(); - } + await this.ListenServerAsync(); + } - protected override async ValueTask DisposeAsync(bool disposing) + protected override async ValueTask DisposeAsync(bool disposing) + { + if (disposing) { - if (disposing) + if (this._smtpServer != null) { - if (this._smtpServer != null) - { - await this.StopServerAsync(); - await this._smtpServer.DisposeAsync(); - } + await this.StopServerAsync(); + await this._smtpServer.DisposeAsync(); } } + } + + private async Task StopServerAsync() + { + await this._smtpServer.StopAsync(); + } - private async Task StopServerAsync() + async Task ListenServerAsync() + { + try { await this._smtpServer.StopAsync(); + await this._smtpServer.StartAsync(new EndpointDefinition(Properties.Settings.Default.IP, Properties.Settings.Default.Port)); + await this._messageBus.PublishAsync(new SmtpServerBindEvent(Properties.Settings.Default.IP, Properties.Settings.Default.Port)); } - - async Task ListenServerAsync() + catch (Exception ex) { - try - { - await this._smtpServer.StopAsync(); - await this._smtpServer.StartAsync(new EndpointDefinition(Properties.Settings.Default.IP, Properties.Settings.Default.Port)); - await this._messageBus.PublishAsync(new SmtpServerBindEvent(Properties.Settings.Default.IP, Properties.Settings.Default.Port)); - } - catch (Exception ex) - { - this._logger.Warning( - ex, - "Failed to bind SMTP to the {Address} {Port} specified. The port may already be in use by another process.", - Properties.Settings.Default.IP, - Properties.Settings.Default.Port); + this._logger.Warning( + ex, + "Failed to bind SMTP to the {Address} {Port} specified. The port may already be in use by another process.", + Properties.Settings.Default.IP, + Properties.Settings.Default.Port); - await this._messageBus.PublishAsync(new SmtpServerBindFailedEvent()); - } + await this._messageBus.PublishAsync(new SmtpServerBindFailedEvent()); } + } - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsSelf() - .AsImplementedInterfaces() - .InstancePerLifetimeScope(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsSelf() + .AsImplementedInterfaces() + .InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/Themes/ThemeManagerService.cs b/src/Papercut.UI/AppLayer/Themes/ThemeManagerService.cs index c0fb05ca..8b83dfbf 100644 --- a/src/Papercut.UI/AppLayer/Themes/ThemeManagerService.cs +++ b/src/Papercut.UI/AppLayer/Themes/ThemeManagerService.cs @@ -27,82 +27,81 @@ using Papercut.Domain.LifecycleHooks; using Papercut.Infrastructure.Themes; -namespace Papercut.AppLayer.Themes +namespace Papercut.AppLayer.Themes; + +public class ThemeManagerService : IAppLifecyclePreStart, IEventHandler { - public class ThemeManagerService : IAppLifecyclePreStart, IEventHandler - { - private readonly ILogger _logger; + private readonly ILogger _logger; - private readonly ThemeColorRepository _themeColorRepository; + private readonly ThemeColorRepository _themeColorRepository; - public ThemeManagerService(ILogger logger, ThemeColorRepository themeColorRepository) - { - this._logger = logger; - this._themeColorRepository = themeColorRepository; - } + public ThemeManagerService(ILogger logger, ThemeColorRepository themeColorRepository) + { + this._logger = logger; + this._themeColorRepository = themeColorRepository; + } - private static ThemeManager CurrentTheme => ThemeManager.Current; + private static ThemeManager CurrentTheme => ThemeManager.Current; - public Task OnPreStart() - { - this.SetTheme(); - return Task.FromResult(AppLifecycleActionResultType.Continue); - } + public Task OnPreStart() + { + this.SetTheme(); + return Task.FromResult(AppLifecycleActionResultType.Continue); + } - public Task HandleAsync(SettingsUpdatedEvent @event, CancellationToken token) - { - if (@event.PreviousSettings.Theme != @event.NewSettings.Theme) this.SetTheme(); + public Task HandleAsync(SettingsUpdatedEvent @event, CancellationToken token) + { + if (@event.PreviousSettings.Theme != @event.NewSettings.Theme) this.SetTheme(); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - private void SetTheme() - { - var colorTheme = this._themeColorRepository.FirstOrDefaultByName(Properties.Settings.Default.Theme); + private void SetTheme() + { + var colorTheme = this._themeColorRepository.FirstOrDefaultByName(Properties.Settings.Default.Theme); - if (colorTheme == null) - { - this._logger.Warning("Unable to find theme color {ThemeColor}. Setting to default: LightBlue.", Properties.Settings.Default.Theme); - Properties.Settings.Default.Theme = "LightBlue"; - return; - } + if (colorTheme == null) + { + this._logger.Warning("Unable to find theme color {ThemeColor}. Setting to default: LightBlue.", Properties.Settings.Default.Theme); + Properties.Settings.Default.Theme = "LightBlue"; + return; + } - var themeColor = colorTheme.Color; + var themeColor = colorTheme.Color; - var theme = CurrentTheme.DetectTheme(Application.Current); - if (theme != null) + var theme = CurrentTheme.DetectTheme(Application.Current); + if (theme != null) + { + var inverseTheme = CurrentTheme.GetInverseTheme(theme); + if (inverseTheme != null) { - var inverseTheme = CurrentTheme.GetInverseTheme(theme); - if (inverseTheme != null) - { - var runtimeTheme = RuntimeThemeGenerator.Current.GenerateRuntimeTheme(inverseTheme.BaseColorScheme, themeColor); - if (runtimeTheme != null) CurrentTheme.AddTheme(runtimeTheme); - } - - var generateRuntimeTheme = RuntimeThemeGenerator.Current.GenerateRuntimeTheme(theme.BaseColorScheme, themeColor); - if (generateRuntimeTheme != null) - CurrentTheme.ChangeTheme( - Application.Current, - CurrentTheme.AddTheme(generateRuntimeTheme)); + var runtimeTheme = RuntimeThemeGenerator.Current.GenerateRuntimeTheme(inverseTheme.BaseColorScheme, themeColor); + if (runtimeTheme != null) CurrentTheme.AddTheme(runtimeTheme); } - Application.Current?.MainWindow?.Activate(); + var generateRuntimeTheme = RuntimeThemeGenerator.Current.GenerateRuntimeTheme(theme.BaseColorScheme, themeColor); + if (generateRuntimeTheme != null) + CurrentTheme.ChangeTheme( + Application.Current, + CurrentTheme.AddTheme(generateRuntimeTheme)); } - #region Begin Static Container Registrations + Application.Current?.MainWindow?.Activate(); + } - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/AppLayer/UiCommands/UiCommandHub.cs b/src/Papercut.UI/AppLayer/UiCommands/UiCommandHub.cs index 7aa2202b..cf214305 100644 --- a/src/Papercut.UI/AppLayer/UiCommands/UiCommandHub.cs +++ b/src/Papercut.UI/AppLayer/UiCommands/UiCommandHub.cs @@ -26,86 +26,85 @@ using Papercut.Domain.UiCommands; using Papercut.Domain.UiCommands.Commands; -namespace Papercut.AppLayer.UiCommands +namespace Papercut.AppLayer.UiCommands; + +public class UiCommandHub : Disposable, IUiCommandHub, IEventHandler { - public class UiCommandHub : Disposable, IUiCommandHub, IEventHandler - { - private readonly Subject _onShowBalloonTip = new Subject(); + private readonly Subject _onShowBalloonTip = new Subject(); - private readonly Subject _onShowMainWindow = new Subject(); + private readonly Subject _onShowMainWindow = new Subject(); - private readonly Subject _onShowMessage = new Subject(); + private readonly Subject _onShowMessage = new Subject(); - private readonly Subject _onShowOptionWindow = new Subject(); + private readonly Subject _onShowOptionWindow = new Subject(); - public IObservable OnShowBalloonTip => this._onShowBalloonTip; + public IObservable OnShowBalloonTip => this._onShowBalloonTip; - public IObservable OnShowMainWindow => this._onShowMainWindow; + public IObservable OnShowMainWindow => this._onShowMainWindow; - public IObservable OnShowMessage => this._onShowMessage; + public IObservable OnShowMessage => this._onShowMessage; - public IObservable OnShowOptionWindow => this._onShowOptionWindow; + public IObservable OnShowOptionWindow => this._onShowOptionWindow; - public Task HandleAsync(ShowMainWindowCommand @event, CancellationToken token = default) - { - this._onShowMainWindow.OnNext(@event); + public Task HandleAsync(ShowMainWindowCommand @event, CancellationToken token = default) + { + this._onShowMainWindow.OnNext(@event); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public void ShowBalloonTip(int timeout, string tipTitle, string tipText, ToolTipIcon toolTipIcon) + public void ShowBalloonTip(int timeout, string tipTitle, string tipText, ToolTipIcon toolTipIcon) + { + if (Properties.Settings.Default.ShowNotifications) { - if (Properties.Settings.Default.ShowNotifications) - { - var command = new ShowBalloonTipCommand(timeout, tipTitle, tipText, toolTipIcon); - this._onShowBalloonTip.OnNext(command); - } + var command = new ShowBalloonTipCommand(timeout, tipTitle, tipText, toolTipIcon); + this._onShowBalloonTip.OnNext(command); } + } - public void ShowMainWindow(bool selectMostRecentMessage = false) - { - var command = new ShowMainWindowCommand(selectMostRecentMessage); + public void ShowMainWindow(bool selectMostRecentMessage = false) + { + var command = new ShowMainWindowCommand(selectMostRecentMessage); - this._onShowMainWindow.OnNext(command); - } + this._onShowMainWindow.OnNext(command); + } - public void ShowMessage(string messageText, string caption) - { - var command = new ShowMessageCommand(messageText, caption); - this._onShowMessage.OnNext(command); - } + public void ShowMessage(string messageText, string caption) + { + var command = new ShowMessageCommand(messageText, caption); + this._onShowMessage.OnNext(command); + } - public void ShowOptionWindow() - { - var command = new ShowOptionWindowCommand(); - this._onShowOptionWindow.OnNext(command); - } + public void ShowOptionWindow() + { + var command = new ShowOptionWindowCommand(); + this._onShowOptionWindow.OnNext(command); + } - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - this._onShowBalloonTip.Dispose(); - this._onShowMainWindow.Dispose(); - this._onShowMessage.Dispose(); - this._onShowOptionWindow.Dispose(); - } + this._onShowBalloonTip.Dispose(); + this._onShowMainWindow.Dispose(); + this._onShowMessage.Dispose(); + this._onShowOptionWindow.Dispose(); } + } - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().As().As>().SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().As().As>().SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/AppCommands/IAppCommandHub.cs b/src/Papercut.UI/Domain/AppCommands/IAppCommandHub.cs index ed0b92ec..eaaf61b3 100644 --- a/src/Papercut.UI/Domain/AppCommands/IAppCommandHub.cs +++ b/src/Papercut.UI/Domain/AppCommands/IAppCommandHub.cs @@ -16,12 +16,11 @@ // limitations under the License. -namespace Papercut.Domain.AppCommands +namespace Papercut.Domain.AppCommands; + +public interface IAppCommandHub { - public interface IAppCommandHub - { - IObservable OnShutdown { get; } + IObservable OnShutdown { get; } - void Shutdown(int exitCode = 0); - } + void Shutdown(int exitCode = 0); } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/AppCommands/ShutdownCommand.cs b/src/Papercut.UI/Domain/AppCommands/ShutdownCommand.cs index 016067e2..7c4d7967 100644 --- a/src/Papercut.UI/Domain/AppCommands/ShutdownCommand.cs +++ b/src/Papercut.UI/Domain/AppCommands/ShutdownCommand.cs @@ -18,15 +18,14 @@ using Papercut.Common.Domain; -namespace Papercut.Domain.AppCommands +namespace Papercut.Domain.AppCommands; + +public class ShutdownCommand : ICommand { - public class ShutdownCommand : ICommand + public ShutdownCommand(int exitCode) { - public ShutdownCommand(int exitCode) - { - this.ExitCode = exitCode; - } - - public int ExitCode { get; } + this.ExitCode = exitCode; } + + public int ExitCode { get; } } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/BackendService/BackendServiceStatus.cs b/src/Papercut.UI/Domain/BackendService/BackendServiceStatus.cs index cacc5a30..34b4d451 100644 --- a/src/Papercut.UI/Domain/BackendService/BackendServiceStatus.cs +++ b/src/Papercut.UI/Domain/BackendService/BackendServiceStatus.cs @@ -16,10 +16,9 @@ // limitations under the License. -namespace Papercut.Domain.BackendService +namespace Papercut.Domain.BackendService; + +public interface IBackendServiceStatus { - public interface IBackendServiceStatus - { - bool IsOnline { get; } - } + bool IsOnline { get; } } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/Events/PapercutServiceStatusEvent.cs b/src/Papercut.UI/Domain/Events/PapercutServiceStatusEvent.cs index df92833a..08796697 100644 --- a/src/Papercut.UI/Domain/Events/PapercutServiceStatusEvent.cs +++ b/src/Papercut.UI/Domain/Events/PapercutServiceStatusEvent.cs @@ -18,21 +18,20 @@ using Papercut.Common.Domain; -namespace Papercut.Domain.Events +namespace Papercut.Domain.Events; + +public class PapercutServiceStatusEvent : IEvent { - public class PapercutServiceStatusEvent : IEvent + public PapercutServiceStatusEvent(PapercutServiceStatusType papercutServiceStatus) { - public PapercutServiceStatusEvent(PapercutServiceStatusType papercutServiceStatus) - { - this.PapercutServiceStatus = papercutServiceStatus; - } - - public PapercutServiceStatusType PapercutServiceStatus { get; } + this.PapercutServiceStatus = papercutServiceStatus; } - public enum PapercutServiceStatusType - { - Offline = 0, - Online = 1 - } + public PapercutServiceStatusType PapercutServiceStatus { get; } +} + +public enum PapercutServiceStatusType +{ + Offline = 0, + Online = 1 } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/Events/SettingsUpdatedEvent.cs b/src/Papercut.UI/Domain/Events/SettingsUpdatedEvent.cs index e9c18d8f..451c4633 100644 --- a/src/Papercut.UI/Domain/Events/SettingsUpdatedEvent.cs +++ b/src/Papercut.UI/Domain/Events/SettingsUpdatedEvent.cs @@ -19,12 +19,11 @@ using Papercut.Common.Domain; using Papercut.Properties; -namespace Papercut.Domain.Events +namespace Papercut.Domain.Events; + +public class SettingsUpdatedEvent(Settings previousSettings, Settings? newSettings = null) : IEvent { - public class SettingsUpdatedEvent(Settings previousSettings, Settings? newSettings = null) : IEvent - { - public Settings PreviousSettings { get; } = previousSettings; + public Settings PreviousSettings { get; } = previousSettings; - public Settings NewSettings { get; } = newSettings ?? Settings.Default; - } + public Settings NewSettings { get; } = newSettings ?? Settings.Default; } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/HtmlPreviews/IHtmlPreviewGenerator.cs b/src/Papercut.UI/Domain/HtmlPreviews/IHtmlPreviewGenerator.cs index 92cc525f..66388660 100644 --- a/src/Papercut.UI/Domain/HtmlPreviews/IHtmlPreviewGenerator.cs +++ b/src/Papercut.UI/Domain/HtmlPreviews/IHtmlPreviewGenerator.cs @@ -18,12 +18,11 @@ using MimeKit; -namespace Papercut.Domain.HtmlPreviews +namespace Papercut.Domain.HtmlPreviews; + +public interface IHtmlPreviewGenerator { - public interface IHtmlPreviewGenerator - { - string GetHtmlPreview(MimeMessage? mailMessageEx, string? tempDir = null); + string GetHtmlPreview(MimeMessage? mailMessageEx, string? tempDir = null); - string? GetHtmlPreviewFile(MimeMessage? mailMessageEx, string? tempDir = null); - } + string? GetHtmlPreviewFile(MimeMessage? mailMessageEx, string? tempDir = null); } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/LifecycleHooks/AppLifecycleActionResultType.cs b/src/Papercut.UI/Domain/LifecycleHooks/AppLifecycleActionResultType.cs index 41898e94..f045d015 100644 --- a/src/Papercut.UI/Domain/LifecycleHooks/AppLifecycleActionResultType.cs +++ b/src/Papercut.UI/Domain/LifecycleHooks/AppLifecycleActionResultType.cs @@ -16,11 +16,10 @@ // limitations under the License. -namespace Papercut.Domain.LifecycleHooks +namespace Papercut.Domain.LifecycleHooks; + +public enum AppLifecycleActionResultType { - public enum AppLifecycleActionResultType - { - Continue = 1, - Cancel = 10 - } + Continue = 1, + Cancel = 10 } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecycleHook.cs b/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecycleHook.cs index 095bddb4..a49d45d8 100644 --- a/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecycleHook.cs +++ b/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecycleHook.cs @@ -16,8 +16,7 @@ // limitations under the License. -namespace Papercut.Domain.LifecycleHooks -{ - public interface IAppLifecycleHook - {} -} \ No newline at end of file +namespace Papercut.Domain.LifecycleHooks; + +public interface IAppLifecycleHook +{} \ No newline at end of file diff --git a/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecyclePreExit.cs b/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecyclePreExit.cs index 401c89f5..9ff67aa9 100644 --- a/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecyclePreExit.cs +++ b/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecyclePreExit.cs @@ -16,10 +16,9 @@ // limitations under the License. -namespace Papercut.Domain.LifecycleHooks +namespace Papercut.Domain.LifecycleHooks; + +public interface IAppLifecyclePreExit : IAppLifecycleHook { - public interface IAppLifecyclePreExit : IAppLifecycleHook - { - Task OnPreExit(); - } + Task OnPreExit(); } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecyclePreStart.cs b/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecyclePreStart.cs index 2e55f4d2..344e44ee 100644 --- a/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecyclePreStart.cs +++ b/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecyclePreStart.cs @@ -16,10 +16,9 @@ // limitations under the License. -namespace Papercut.Domain.LifecycleHooks +namespace Papercut.Domain.LifecycleHooks; + +public interface IAppLifecyclePreStart : IAppLifecycleHook { - public interface IAppLifecyclePreStart : IAppLifecycleHook - { - Task OnPreStart(); - } + Task OnPreStart(); } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecycleStarted.cs b/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecycleStarted.cs index ace02cf9..f0e52ac5 100644 --- a/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecycleStarted.cs +++ b/src/Papercut.UI/Domain/LifecycleHooks/IAppLifecycleStarted.cs @@ -16,10 +16,9 @@ // limitations under the License. -namespace Papercut.Domain.LifecycleHooks +namespace Papercut.Domain.LifecycleHooks; + +public interface IAppLifecycleStarted : IAppLifecycleHook { - public interface IAppLifecycleStarted : IAppLifecycleHook - { - Task OnStartedAsync(); - } + Task OnStartedAsync(); } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/UiCommands/Commands/ShowBalloonTipCommand.cs b/src/Papercut.UI/Domain/UiCommands/Commands/ShowBalloonTipCommand.cs index e3f95a18..c5e8a8cd 100644 --- a/src/Papercut.UI/Domain/UiCommands/Commands/ShowBalloonTipCommand.cs +++ b/src/Papercut.UI/Domain/UiCommands/Commands/ShowBalloonTipCommand.cs @@ -20,24 +20,23 @@ using Papercut.Common.Domain; -namespace Papercut.Domain.UiCommands.Commands +namespace Papercut.Domain.UiCommands.Commands; + +public class ShowBalloonTipCommand : ICommand { - public class ShowBalloonTipCommand : ICommand + public ShowBalloonTipCommand(int timeout, string tipTitle, string tipText, ToolTipIcon toolTipIcon) { - public ShowBalloonTipCommand(int timeout, string tipTitle, string tipText, ToolTipIcon toolTipIcon) - { - this.Timeout = timeout; - this.TipTitle = tipTitle; - this.TipText = tipText; - this.ToolTipIcon = toolTipIcon; - } + this.Timeout = timeout; + this.TipTitle = tipTitle; + this.TipText = tipText; + this.ToolTipIcon = toolTipIcon; + } - public int Timeout { get; set; } + public int Timeout { get; set; } - public string TipTitle { get; set; } + public string TipTitle { get; set; } - public string TipText { get; set; } + public string TipText { get; set; } - public ToolTipIcon ToolTipIcon { get; set; } - } + public ToolTipIcon ToolTipIcon { get; set; } } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/UiCommands/Commands/ShowMainWindowCommand.cs b/src/Papercut.UI/Domain/UiCommands/Commands/ShowMainWindowCommand.cs index 7e46d8a3..3c139acb 100644 --- a/src/Papercut.UI/Domain/UiCommands/Commands/ShowMainWindowCommand.cs +++ b/src/Papercut.UI/Domain/UiCommands/Commands/ShowMainWindowCommand.cs @@ -18,15 +18,9 @@ using Papercut.Common.Domain; -namespace Papercut.Domain.UiCommands.Commands -{ - public class ShowMainWindowCommand : IEvent - { - public ShowMainWindowCommand(bool selectMostRecentMessage = false) - { - this.SelectMostRecentMessage = selectMostRecentMessage; - } +namespace Papercut.Domain.UiCommands.Commands; - public bool SelectMostRecentMessage { get; set; } - } +public class ShowMainWindowCommand(bool selectMostRecentMessage = false) : IEvent +{ + public bool SelectMostRecentMessage { get; set; } = selectMostRecentMessage; } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/UiCommands/Commands/ShowMessageCommand.cs b/src/Papercut.UI/Domain/UiCommands/Commands/ShowMessageCommand.cs index 39a27d5a..86cd4c8a 100644 --- a/src/Papercut.UI/Domain/UiCommands/Commands/ShowMessageCommand.cs +++ b/src/Papercut.UI/Domain/UiCommands/Commands/ShowMessageCommand.cs @@ -18,18 +18,11 @@ using Papercut.Common.Domain; -namespace Papercut.Domain.UiCommands.Commands -{ - public class ShowMessageCommand : ICommand - { - public ShowMessageCommand(string messageText, string caption) - { - this.MessageText = messageText; - this.Caption = caption; - } +namespace Papercut.Domain.UiCommands.Commands; - public string MessageText { get; set; } +public class ShowMessageCommand(string messageText, string caption) : ICommand +{ + public string MessageText { get; set; } = messageText; - public string Caption { get; set; } - } + public string Caption { get; set; } = caption; } \ No newline at end of file diff --git a/src/Papercut.UI/Domain/UiCommands/Commands/ShowOptionWindowCommand.cs b/src/Papercut.UI/Domain/UiCommands/Commands/ShowOptionWindowCommand.cs index ac55ea43..85d7c5d5 100644 --- a/src/Papercut.UI/Domain/UiCommands/Commands/ShowOptionWindowCommand.cs +++ b/src/Papercut.UI/Domain/UiCommands/Commands/ShowOptionWindowCommand.cs @@ -18,8 +18,7 @@ using Papercut.Common.Domain; -namespace Papercut.Domain.UiCommands.Commands -{ - public class ShowOptionWindowCommand : ICommand - {} -} \ No newline at end of file +namespace Papercut.Domain.UiCommands.Commands; + +public class ShowOptionWindowCommand : ICommand +{} \ No newline at end of file diff --git a/src/Papercut.UI/Domain/UiCommands/IUiCommandHub.cs b/src/Papercut.UI/Domain/UiCommands/IUiCommandHub.cs index 47de80cf..23aa6c8c 100644 --- a/src/Papercut.UI/Domain/UiCommands/IUiCommandHub.cs +++ b/src/Papercut.UI/Domain/UiCommands/IUiCommandHub.cs @@ -20,24 +20,23 @@ using Papercut.Domain.UiCommands.Commands; -namespace Papercut.Domain.UiCommands +namespace Papercut.Domain.UiCommands; + +public interface IUiCommandHub { - public interface IUiCommandHub - { - IObservable OnShowBalloonTip { get; } + IObservable OnShowBalloonTip { get; } - IObservable OnShowOptionWindow { get; } + IObservable OnShowOptionWindow { get; } - IObservable OnShowMessage { get; } + IObservable OnShowMessage { get; } - IObservable OnShowMainWindow { get; } + IObservable OnShowMainWindow { get; } - void ShowMainWindow(bool selectMostRecentMessage = false); + void ShowMainWindow(bool selectMostRecentMessage = false); - void ShowMessage(string messageText, string caption); + void ShowMessage(string messageText, string caption); - void ShowOptionWindow(); + void ShowOptionWindow(); - void ShowBalloonTip(int timeout, string tipTitle, string tipText, ToolTipIcon toolTipIcon); - } + void ShowBalloonTip(int timeout, string tipTitle, string tipText, ToolTipIcon toolTipIcon); } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/CalburnSerilogBridge.cs b/src/Papercut.UI/Helpers/CalburnSerilogBridge.cs index 15211a6e..72618c6e 100644 --- a/src/Papercut.UI/Helpers/CalburnSerilogBridge.cs +++ b/src/Papercut.UI/Helpers/CalburnSerilogBridge.cs @@ -18,30 +18,29 @@ using Caliburn.Micro; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public class CalburnSerilogBridge : ILog { - public class CalburnSerilogBridge : ILog + readonly Lazy _logger; + + public CalburnSerilogBridge(Lazy logger) + { + this._logger = logger; + } + + public void Info(string format, params object[] args) + { + if (!format.StartsWith("Action Convention Not Applied")) this._logger.Value.Verbose(format, args); + } + + public void Warn(string format, params object[] args) + { + this._logger.Value.Warning(format, args); + } + + public void Error(Exception exception) { - readonly Lazy _logger; - - public CalburnSerilogBridge(Lazy logger) - { - this._logger = logger; - } - - public void Info(string format, params object[] args) - { - if (!format.StartsWith("Action Convention Not Applied")) this._logger.Value.Verbose(format, args); - } - - public void Warn(string format, params object[] args) - { - this._logger.Value.Warning(format, args); - } - - public void Error(Exception exception) - { - this._logger.Value.Error(exception, "Exception Logged"); - } + this._logger.Value.Error(exception, "Exception Logged"); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/DisableEdgeFeaturesHelper.cs b/src/Papercut.UI/Helpers/DisableEdgeFeaturesHelper.cs index 95d702fd..cfbef905 100644 --- a/src/Papercut.UI/Helpers/DisableEdgeFeaturesHelper.cs +++ b/src/Papercut.UI/Helpers/DisableEdgeFeaturesHelper.cs @@ -18,22 +18,21 @@ using Microsoft.Web.WebView2.Core; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public static class DisableEdgeFeaturesHelper { - public static class DisableEdgeFeaturesHelper + public static void DisableEdgeFeatures(this CoreWebView2 coreWeb) { - public static void DisableEdgeFeatures(this CoreWebView2 coreWeb) - { - ArgumentNullException.ThrowIfNull(coreWeb); + ArgumentNullException.ThrowIfNull(coreWeb); - coreWeb.Settings.AreDefaultContextMenusEnabled = false; - coreWeb.Settings.IsZoomControlEnabled = false; - coreWeb.Settings.AreDevToolsEnabled = false; - coreWeb.Settings.AreDefaultScriptDialogsEnabled = false; - coreWeb.Settings.IsBuiltInErrorPageEnabled = false; + coreWeb.Settings.AreDefaultContextMenusEnabled = false; + coreWeb.Settings.IsZoomControlEnabled = false; + coreWeb.Settings.AreDevToolsEnabled = false; + coreWeb.Settings.AreDefaultScriptDialogsEnabled = false; + coreWeb.Settings.IsBuiltInErrorPageEnabled = false; - // Issue #145 fixed - coreWeb.Settings.IsStatusBarEnabled = true; - } + // Issue #145 fixed + coreWeb.Settings.IsStatusBarEnabled = true; } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/EventHandlerHelpers.cs b/src/Papercut.UI/Helpers/EventHandlerHelpers.cs index 1d7a4b39..515a5745 100644 --- a/src/Papercut.UI/Helpers/EventHandlerHelpers.cs +++ b/src/Papercut.UI/Helpers/EventHandlerHelpers.cs @@ -19,27 +19,26 @@ using System.Reactive; using System.Reactive.Linq; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public static class EventHandlerHelpers { - public static class EventHandlerHelpers + public static IObservable> ToObservable(this EventHandler eventHandler) { - public static IObservable> ToObservable(this EventHandler eventHandler) - { - ArgumentNullException.ThrowIfNull(eventHandler); + ArgumentNullException.ThrowIfNull(eventHandler); - return Observable.FromEventPattern( - a => eventHandler += a, - d => eventHandler += d); - } + return Observable.FromEventPattern( + a => eventHandler += a, + d => eventHandler += d); + } - public static IObservable> ToObservable(this EventHandler eventHandler) - where TArgs : EventArgs - { - ArgumentNullException.ThrowIfNull(eventHandler); + public static IObservable> ToObservable(this EventHandler eventHandler) + where TArgs : EventArgs + { + ArgumentNullException.ThrowIfNull(eventHandler); - return Observable.FromEventPattern, TArgs>( - a => eventHandler += a, - d => eventHandler += d); - } + return Observable.FromEventPattern, TArgs>( + a => eventHandler += a, + d => eventHandler += d); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/HtmlHelpers.cs b/src/Papercut.UI/Helpers/HtmlHelpers.cs index 0bfb508c..c1cf8738 100644 --- a/src/Papercut.UI/Helpers/HtmlHelpers.cs +++ b/src/Papercut.UI/Helpers/HtmlHelpers.cs @@ -18,35 +18,34 @@ using System.Text.RegularExpressions; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public static class HtmlHelpers { - public static class HtmlHelpers - { - private static readonly Regex _uriRegex = new Regex( - @"(((?http(s)?):\/\/)([\w-]+?\.\w+)+([a-zA-Z0-9\~\!\@\#\$\%\^\&\;\*\(\)_\-\=\+\\\/\?\.\:\;\,]*)?)", - RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly Regex _uriRegex = new Regex( + @"(((?http(s)?):\/\/)([\w-]+?\.\w+)+([a-zA-Z0-9\~\!\@\#\$\%\^\&\;\*\(\)_\-\=\+\\\/\?\.\:\;\,]*)?)", + RegexOptions.Compiled | RegexOptions.Multiline); - public static string Linkify(this string text, string target = "_self") - { - return _uriRegex.Replace( - text, - match => + public static string Linkify(this string text, string target = "_self") + { + return _uriRegex.Replace( + text, + match => + { + try { - try - { - var link = match.ToString(); - var scheme = match.Groups["scheme"].Value == "https" ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + var link = match.ToString(); + var scheme = match.Groups["scheme"].Value == "https" ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; - var url = new UriBuilder(link) { Scheme = scheme }.Uri.ToString(); + var url = new UriBuilder(link) { Scheme = scheme }.Uri.ToString(); - return $@"{link}"; - } - catch (Exception) - { - return match.ToString(); - } + return $@"{link}"; + } + catch (Exception) + { + return match.ToString(); } - ); - } + } + ); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/HtmlPreviewVisitor.cs b/src/Papercut.UI/Helpers/HtmlPreviewVisitor.cs index e4f7b245..a58b652a 100644 --- a/src/Papercut.UI/Helpers/HtmlPreviewVisitor.cs +++ b/src/Papercut.UI/Helpers/HtmlPreviewVisitor.cs @@ -24,306 +24,305 @@ using Papercut.Properties; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +internal class HtmlPreviewVisitor : MimeVisitor { - internal class HtmlPreviewVisitor : MimeVisitor - { - const int IndexNotFound = -1; + const int IndexNotFound = -1; + + private static readonly Dictionary _mimeLookup + = new Dictionary() + { + { "image/jpeg", "jpg" }, + { "image/svg+xml", "svg" } + }; + + readonly List _attachments = new List(); - private static readonly Dictionary _mimeLookup - = new Dictionary() - { - { "image/jpeg", "jpg" }, - { "image/svg+xml", "svg" } - }; + readonly List _stack = new List(); + + string _body; + + public HtmlPreviewVisitor(string? tempDirectory = null) + { + this.TempDirectory = tempDirectory; + } - readonly List _attachments = new List(); + public string? TempDirectory { get; } - readonly List _stack = new List(); + public IList Attachments => this._attachments; - string _body; + public string HtmlBody => this._body ?? string.Empty; - public HtmlPreviewVisitor(string? tempDirectory = null) + protected override void VisitMultipartAlternative(MultipartAlternative alternative) + { + // walk the multipart/alternative children backwards from the greatest level of faithfulness to the least faithful + for (int i = alternative.Count - 1; i >= 0 && this._body == null; i--) { - this.TempDirectory = tempDirectory; + alternative[i].Accept(this); } + } + + protected override void VisitMultipartRelated(MultipartRelated related) + { + var root = related.Root; + this._stack.Add(related); + root.Accept(this); + this._stack.RemoveAt(this._stack.Count - 1); + } - public string? TempDirectory { get; } + bool TryGetImage(string url, out MimePart? image) + { + image = null; - public IList Attachments => this._attachments; + UriKind kind; + Uri uri; - public string HtmlBody => this._body ?? string.Empty; + if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) + kind = UriKind.Absolute; + else if (Uri.IsWellFormedUriString(url, UriKind.Relative)) + kind = UriKind.Relative; + else + kind = UriKind.RelativeOrAbsolute; - protected override void VisitMultipartAlternative(MultipartAlternative alternative) + try { - // walk the multipart/alternative children backwards from the greatest level of faithfulness to the least faithful - for (int i = alternative.Count - 1; i >= 0 && this._body == null; i--) - { - alternative[i].Accept(this); - } + uri = new Uri(url, kind); } - - protected override void VisitMultipartRelated(MultipartRelated related) + catch (UriFormatException) { - var root = related.Root; - this._stack.Add(related); - root.Accept(this); - this._stack.RemoveAt(this._stack.Count - 1); + return false; } - bool TryGetImage(string url, out MimePart? image) + foreach (var item in this._stack.ToArray().Reverse()) { - image = null; + int index = item.IndexOf(uri); - UriKind kind; - Uri uri; - - if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) - kind = UriKind.Absolute; - else if (Uri.IsWellFormedUriString(url, UriKind.Relative)) - kind = UriKind.Relative; - else - kind = UriKind.RelativeOrAbsolute; + if (index == IndexNotFound) + continue; - try - { - uri = new Uri(url, kind); - } - catch (UriFormatException) - { - return false; - } + image = item[index] as MimePart; - foreach (var item in this._stack.ToArray().Reverse()) - { - int index = item.IndexOf(uri); + return image != null; + } - if (index == IndexNotFound) - continue; + return false; + } - image = item[index] as MimePart; + string SaveImage(MimePart image, string url) + { + string fileName = url.Replace(':', '_').Replace('\\', '_').Replace('/', '_'); + var mimeExt = GetExtensionFromMimeType(image.ContentType.MimeType); - return image != null; - } + string path = Path.Combine(this.TempDirectory, $"{fileName}.{mimeExt}"); - return false; - } - - string SaveImage(MimePart image, string url) + if (!File.Exists(path)) { - string fileName = url.Replace(':', '_').Replace('\\', '_').Replace('/', '_'); - var mimeExt = GetExtensionFromMimeType(image.ContentType.MimeType); - - string path = Path.Combine(this.TempDirectory, $"{fileName}.{mimeExt}"); + using var output = File.Create(path); + image.Content.DecodeTo(output); + } - if (!File.Exists(path)) - { - using var output = File.Create(path); - image.Content.DecodeTo(output); - } + return $"file://{path.Replace('\\', '/')}"; + } - return $"file://{path.Replace('\\', '/')}"; - } + private static string GetExtensionFromMimeType(string mimeType) + { + // try to add a file extension for niceness + mimeType = mimeType.ToLowerInvariant(); - private static string GetExtensionFromMimeType(string mimeType) + foreach (var pair in _mimeLookup.Where(pair => mimeType.StartsWith(pair.Key))) { - // try to add a file extension for niceness - mimeType = mimeType.ToLowerInvariant(); + // match + return pair.Value; + } - foreach (var pair in _mimeLookup.Where(pair => mimeType.StartsWith(pair.Key))) - { - // match - return pair.Value; - } + // take the value after the split + return mimeType.Split('/').LastOrDefault(); + } - // take the value after the split - return mimeType.Split('/').LastOrDefault(); + void HtmlTagCallback(HtmlTagContext ctx, HtmlWriter htmlWriter) + { + void WriteTagAsIs() + { + // pass the tag through to the output + ctx.WriteTag(htmlWriter, true); } - void HtmlTagCallback(HtmlTagContext ctx, HtmlWriter htmlWriter) + switch (ctx.TagId) { - void WriteTagAsIs() - { - // pass the tag through to the output - ctx.WriteTag(htmlWriter, true); - } + case HtmlTagId.Head when !ctx.IsEndTag: + ctx.WriteTag(htmlWriter, false); + this.AddMetaCompatibleIEEdge(htmlWriter); - switch (ctx.TagId) - { - case HtmlTagId.Head when !ctx.IsEndTag: - ctx.WriteTag(htmlWriter, false); - this.AddMetaCompatibleIEEdge(htmlWriter); - - break; + break; - case HtmlTagId.Image when !ctx.IsEndTag && this._stack.Count > 0: - this.LinkImageTag(ctx, htmlWriter); - - break; - case HtmlTagId.Body when !ctx.IsEndTag: - RemoveContextMenuFromBodyTag(ctx, htmlWriter); - break; - default: + case HtmlTagId.Image when !ctx.IsEndTag && this._stack.Count > 0: + this.LinkImageTag(ctx, htmlWriter); + + break; + case HtmlTagId.Body when !ctx.IsEndTag: + RemoveContextMenuFromBodyTag(ctx, htmlWriter); + break; + default: - WriteTagAsIs(); + WriteTagAsIs(); - break; - } + break; } + } - private void AddMetaCompatibleIEEdge(HtmlWriter htmlWriter) - { - htmlWriter.WriteStartTag(HtmlTagId.Meta); - htmlWriter.WriteAttribute("http-equiv", "X-UA-Compatible"); - htmlWriter.WriteAttribute("content", "IE=Edge"); - htmlWriter.WriteEndTag(HtmlTagId.Meta); - } + private void AddMetaCompatibleIEEdge(HtmlWriter htmlWriter) + { + htmlWriter.WriteStartTag(HtmlTagId.Meta); + htmlWriter.WriteAttribute("http-equiv", "X-UA-Compatible"); + htmlWriter.WriteAttribute("content", "IE=Edge"); + htmlWriter.WriteEndTag(HtmlTagId.Meta); + } - private static void RemoveContextMenuFromBodyTag(HtmlTagContext ctx, HtmlWriter htmlWriter) + private static void RemoveContextMenuFromBodyTag(HtmlTagContext ctx, HtmlWriter htmlWriter) + { + ctx.WriteTag(htmlWriter, false); + + // add and/or replace oncontextmenu="return false;" + foreach (var attribute in ctx.Attributes) { - ctx.WriteTag(htmlWriter, false); + if (attribute.Name.ToLowerInvariant() == "oncontextmenu") + continue; - // add and/or replace oncontextmenu="return false;" - foreach (var attribute in ctx.Attributes) - { - if (attribute.Name.ToLowerInvariant() == "oncontextmenu") - continue; + htmlWriter.WriteAttribute(attribute); + } - htmlWriter.WriteAttribute(attribute); - } + htmlWriter.WriteAttribute("oncontextmenu", "return false;"); + } - htmlWriter.WriteAttribute("oncontextmenu", "return false;"); - } + private void LinkImageTag(HtmlTagContext ctx, HtmlWriter htmlWriter) + { + ctx.WriteTag(htmlWriter, false); - private void LinkImageTag(HtmlTagContext ctx, HtmlWriter htmlWriter) + // replace the src attribute with a file:// URL + foreach (var attribute in ctx.Attributes) { - ctx.WriteTag(htmlWriter, false); - - // replace the src attribute with a file:// URL - foreach (var attribute in ctx.Attributes) + if (attribute.Id == HtmlAttributeId.Src) { - if (attribute.Id == HtmlAttributeId.Src) + if (!this.TryGetImage(attribute.Value, out MimePart image)) { - if (!this.TryGetImage(attribute.Value, out MimePart image)) - { - htmlWriter.WriteAttribute(attribute); - continue; - } + htmlWriter.WriteAttribute(attribute); + continue; + } - var url = this.SaveImage(image, attribute.Value); + var url = this.SaveImage(image, attribute.Value); - htmlWriter.WriteAttributeName(attribute.Name); - htmlWriter.WriteAttributeValue(url); - } - else - htmlWriter.WriteAttribute(attribute); + htmlWriter.WriteAttributeName(attribute.Name); + htmlWriter.WriteAttributeValue(url); } + else + htmlWriter.WriteAttribute(attribute); } + } - static (string Before,string After) GetBeforeAfterFormatWrapper(string formatWrapper) - { - //var format = UIStrings.TextToHtmlFormatWrapper; - int index = formatWrapper.IndexOf("{0}"); + static (string Before,string After) GetBeforeAfterFormatWrapper(string formatWrapper) + { + //var format = UIStrings.TextToHtmlFormatWrapper; + int index = formatWrapper.IndexOf("{0}"); - return (formatWrapper.Substring(0, index), formatWrapper.Substring(index + 3)); - } + return (formatWrapper.Substring(0, index), formatWrapper.Substring(index + 3)); + } - protected override void VisitTextPart(TextPart entity) + protected override void VisitTextPart(TextPart entity) + { + if (this._body != null) { - if (this._body != null) - { - // since we've already found the body, treat this as an attachment - this._attachments.Add(entity); - return; - } - - if (entity.IsHtml) - { - this.SetHtmlToBody(entity); - } - else if (entity.IsFlowed) - { - this.SetFlowedHtmlToBody(entity); - } - else - { - this.SetTextToHtml(entity); - } + // since we've already found the body, treat this as an attachment + this._attachments.Add(entity); + return; } - private void SetTextToHtml(TextPart entity) + if (entity.IsHtml) { - var beforeAfter = GetBeforeAfterFormatWrapper(UIStrings.TextToHtmlFormatWrapper); - - var converter = new TextToHtml - { - Header = $"{UIStrings.MarkOfTheWeb}{Environment.NewLine}{beforeAfter.Before}", - HeaderFormat = HeaderFooterFormat.Html, - Footer = beforeAfter.After, - FooterFormat = HeaderFooterFormat.Html - }; - - this._body = converter.Convert(entity.Text); + this.SetHtmlToBody(entity); + } + else if (entity.IsFlowed) + { + this.SetFlowedHtmlToBody(entity); + } + else + { + this.SetTextToHtml(entity); } + } + + private void SetTextToHtml(TextPart entity) + { + var beforeAfter = GetBeforeAfterFormatWrapper(UIStrings.TextToHtmlFormatWrapper); - private void SetFlowedHtmlToBody(TextPart entity) + var converter = new TextToHtml { - var beforeAfter = GetBeforeAfterFormatWrapper(UIStrings.HtmlToHtmlFormatWrapper); + Header = $"{UIStrings.MarkOfTheWeb}{Environment.NewLine}{beforeAfter.Before}", + HeaderFormat = HeaderFooterFormat.Html, + Footer = beforeAfter.After, + FooterFormat = HeaderFooterFormat.Html + }; - var convertor = new FlowedToHtml - { - Header = $"{UIStrings.MarkOfTheWeb}{Environment.NewLine}{beforeAfter.Before}", - HeaderFormat = HeaderFooterFormat.Html, - Footer = beforeAfter.After, - FooterFormat = HeaderFooterFormat.Html - }; + this._body = converter.Convert(entity.Text); + } - if (entity.ContentType.Parameters.TryGetValue("delsp", out string delsp)) - { - convertor.DeleteSpace = delsp.ToLowerInvariant() == "yes"; - } + private void SetFlowedHtmlToBody(TextPart entity) + { + var beforeAfter = GetBeforeAfterFormatWrapper(UIStrings.HtmlToHtmlFormatWrapper); - this._body = convertor.Convert(entity.Text); + var convertor = new FlowedToHtml + { + Header = $"{UIStrings.MarkOfTheWeb}{Environment.NewLine}{beforeAfter.Before}", + HeaderFormat = HeaderFooterFormat.Html, + Footer = beforeAfter.After, + FooterFormat = HeaderFooterFormat.Html + }; + + if (entity.ContentType.Parameters.TryGetValue("delsp", out string delsp)) + { + convertor.DeleteSpace = delsp.ToLowerInvariant() == "yes"; } - private void SetHtmlToBody(TextPart entity) + this._body = convertor.Convert(entity.Text); + } + + private void SetHtmlToBody(TextPart entity) + { + var converter = new HtmlToHtml { - var converter = new HtmlToHtml - { - Header = $"{UIStrings.MarkOfTheWeb}{Environment.NewLine}", - HeaderFormat = HeaderFooterFormat.Html, - HtmlTagCallback = this.HtmlTagCallback - }; + Header = $"{UIStrings.MarkOfTheWeb}{Environment.NewLine}", + HeaderFormat = HeaderFooterFormat.Html, + HtmlTagCallback = this.HtmlTagCallback + }; - var html = entity.Text; + var html = entity.Text; - if (!html.Contains("") || !html.Contains("")) - { - var beforeAfter = GetBeforeAfterFormatWrapper(UIStrings.HtmlToHtmlFormatWrapper); + if (!html.Contains("") || !html.Contains("")) + { + var beforeAfter = GetBeforeAfterFormatWrapper(UIStrings.HtmlToHtmlFormatWrapper); - this._body = converter.Convert(beforeAfter.Before + html + beforeAfter.After); + this._body = converter.Convert(beforeAfter.Before + html + beforeAfter.After); - } - else this._body = converter.Convert(html); } + else this._body = converter.Convert(html); + } - protected override void VisitTnefPart(TnefPart entity) - { - // extract any attachments in the MS-TNEF part - this._attachments.AddRange(entity.ExtractAttachments()); - } + protected override void VisitTnefPart(TnefPart entity) + { + // extract any attachments in the MS-TNEF part + this._attachments.AddRange(entity.ExtractAttachments()); + } - protected override void VisitMessagePart(MessagePart entity) - { - // treat message/rfc822 parts as attachments - this._attachments.Add(entity); - } + protected override void VisitMessagePart(MessagePart entity) + { + // treat message/rfc822 parts as attachments + this._attachments.Add(entity); + } - protected override void VisitMimePart(MimePart entity) - { - // realistically, if we've gotten this far, then we can treat this as an attachment - // even if the IsAttachment property is false. - this._attachments.Add(entity); - } + protected override void VisitMimePart(MimePart entity) + { + // realistically, if we've gotten this far, then we can treat this as an attachment + // even if the IsAttachment property is false. + this._attachments.Add(entity); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/IViewModelWindowManager.cs b/src/Papercut.UI/Helpers/IViewModelWindowManager.cs index f8aa198d..6f8ae337 100644 --- a/src/Papercut.UI/Helpers/IViewModelWindowManager.cs +++ b/src/Papercut.UI/Helpers/IViewModelWindowManager.cs @@ -18,23 +18,22 @@ using Caliburn.Micro; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public interface IViewModelWindowManager : IWindowManager { - public interface IViewModelWindowManager : IWindowManager - { - Task ShowDialogWithViewModel( - Action? setViewModel = null, - object? context = null) - where TViewModel : PropertyChangedBase; + Task ShowDialogWithViewModel( + Action? setViewModel = null, + object? context = null) + where TViewModel : PropertyChangedBase; - Task ShowWindowWithViewModelAsync( - Action? setViewModel = null, - object? context = null) - where TViewModel : PropertyChangedBase; + Task ShowWindowWithViewModelAsync( + Action? setViewModel = null, + object? context = null) + where TViewModel : PropertyChangedBase; - Task ShowPopupWithViewModel( - Action? setViewModel = null, - object? context = null) - where TViewModel : PropertyChangedBase; - } + Task ShowPopupWithViewModel( + Action? setViewModel = null, + object? context = null) + where TViewModel : PropertyChangedBase; } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/MailMessageHelper.cs b/src/Papercut.UI/Helpers/MailMessageHelper.cs index b5b52244..e76d1576 100644 --- a/src/Papercut.UI/Helpers/MailMessageHelper.cs +++ b/src/Papercut.UI/Helpers/MailMessageHelper.cs @@ -18,21 +18,20 @@ using System.Net.Mail; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public static class MailMessageHelper { - public static class MailMessageHelper + public static MailMessage CreateFailureMailMessage(string error) { - public static MailMessage CreateFailureMailMessage(string error) + var errorMessage = new MailMessage { - var errorMessage = new MailMessage - { - From = new MailAddress("fail@papercut.com", "Papercut Failure"), - Subject = "Failure loading message: " + error, - Body = "Unable to load", - IsBodyHtml = false - }; + From = new MailAddress("fail@papercut.com", "Papercut Failure"), + Subject = "Failure loading message: " + error, + Body = "Unable to load", + IsBodyHtml = false + }; - return errorMessage; - } + return errorMessage; } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/MessageDetailItemHelper.cs b/src/Papercut.UI/Helpers/MessageDetailItemHelper.cs index 8c489b0e..a103f5d8 100644 --- a/src/Papercut.UI/Helpers/MessageDetailItemHelper.cs +++ b/src/Papercut.UI/Helpers/MessageDetailItemHelper.cs @@ -20,30 +20,29 @@ using Papercut.ViewModels; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public static class MessageDetailItemHelper { - public static class MessageDetailItemHelper + public static Conductor.Collection.OneActive GetConductor(this T messageDetailItem) + where T : Screen, IMessageDetailItem { - public static Conductor.Collection.OneActive GetConductor(this T messageDetailItem) - where T : Screen, IMessageDetailItem - { - return messageDetailItem.Parent as Conductor.Collection.OneActive; - } - - public static async Task ActivateViewModelOf( - this Conductor.Collection.OneActive conductor) - { - ArgumentNullException.ThrowIfNull(conductor); + return messageDetailItem.Parent as Conductor.Collection.OneActive; + } - var item = conductor?.Items.FirstOrDefault(s => s.GetType() == typeof(T)); + public static async Task ActivateViewModelOf( + this Conductor.Collection.OneActive conductor) + { + ArgumentNullException.ThrowIfNull(conductor); - if (item != null) - { - await conductor.ActivateItemAsync(item); - return (T)item; - } + var item = conductor?.Items.FirstOrDefault(s => s.GetType() == typeof(T)); - return default(T); + if (item != null) + { + await conductor.ActivateItemAsync(item); + return (T)item; } + + return default(T); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/MimeMessageEntry.cs b/src/Papercut.UI/Helpers/MimeMessageEntry.cs index ca2062de..6579df21 100644 --- a/src/Papercut.UI/Helpers/MimeMessageEntry.cs +++ b/src/Papercut.UI/Helpers/MimeMessageEntry.cs @@ -21,66 +21,65 @@ using Papercut.Core.Domain.Message; using Papercut.Message; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public class MimeMessageEntry : MessageEntry { - public class MimeMessageEntry : MessageEntry - { - private int _attachmentsCount; + private int _attachmentsCount; - private MessagePriority _priority; + private MessagePriority _priority; - string _subject; + string _subject; - public MimeMessageEntry(MessageEntry entry, MimeMessageLoader loader) - : base(entry.File) - { - this.IsSelected = entry.IsSelected; - this.Subject = "Loading..."; + public MimeMessageEntry(MessageEntry entry, MimeMessageLoader loader) + : base(entry.File) + { + this.IsSelected = entry.IsSelected; + this.Subject = "Loading..."; - loader.GetMessageCallback(this, this.LoadMessageDetails); - } + loader.GetMessageCallback(this, this.LoadMessageDetails); + } - public int AttachmentsCount + public int AttachmentsCount + { + get => this._attachmentsCount; + set { - get => this._attachmentsCount; - set - { - if (value == this._attachmentsCount) return; - this._attachmentsCount = value; - this.OnPropertyChanged(nameof(this.AttachmentsCount)); - this.OnPropertyChanged(nameof(this.HasAttachments)); - } + if (value == this._attachmentsCount) return; + this._attachmentsCount = value; + this.OnPropertyChanged(nameof(this.AttachmentsCount)); + this.OnPropertyChanged(nameof(this.HasAttachments)); } + } - public MessagePriority Priority + public MessagePriority Priority + { + get => this._priority; + protected set { - get => this._priority; - protected set - { - if (value == this._priority) return; - this._priority = value; - this.OnPropertyChanged(nameof(this.Priority)); - } + if (value == this._priority) return; + this._priority = value; + this.OnPropertyChanged(nameof(this.Priority)); } + } - public string Subject + public string Subject + { + get => this._subject; + protected set { - get => this._subject; - protected set - { - if (value == this._subject) return; - this._subject = value; - this.OnPropertyChanged(nameof(this.Subject)); - } + if (value == this._subject) return; + this._subject = value; + this.OnPropertyChanged(nameof(this.Subject)); } + } - public bool HasAttachments => this.AttachmentsCount > 0; + public bool HasAttachments => this.AttachmentsCount > 0; - private void LoadMessageDetails(MimeMessage message) - { - this.Subject = message.Subject; - this.Priority = message.Priority; - this.AttachmentsCount = message.Attachments.Count(); - } + private void LoadMessageDetails(MimeMessage message) + { + this.Subject = message.Subject; + this.Priority = message.Priority; + this.AttachmentsCount = message.Attachments.Count(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/NotifyPropertyChangeReactiveExtensions.cs b/src/Papercut.UI/Helpers/NotifyPropertyChangeReactiveExtensions.cs index 580b12b7..130702b0 100644 --- a/src/Papercut.UI/Helpers/NotifyPropertyChangeReactiveExtensions.cs +++ b/src/Papercut.UI/Helpers/NotifyPropertyChangeReactiveExtensions.cs @@ -23,59 +23,58 @@ using System.Reactive.Linq; using System.Windows.Threading; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public static class NotifyPropertyChangeReactiveExtensions { - public static class NotifyPropertyChangeReactiveExtensions + // Returns the values of property (an Expression) as they + // change, starting with the current value + public static IObservable GetPropertyValues( + this TSource source, + Expression> property, + IScheduler? scheduler = null) + where TSource : INotifyPropertyChanged { - // Returns the values of property (an Expression) as they - // change, starting with the current value - public static IObservable GetPropertyValues( - this TSource source, - Expression> property, - IScheduler? scheduler = null) - where TSource : INotifyPropertyChanged + if (property.Body is not MemberExpression memberExpression) { - if (property.Body is not MemberExpression memberExpression) - { - throw new ArgumentException( - "property must directly access a property of the source"); - } + throw new ArgumentException( + "property must directly access a property of the source"); + } - string propertyName = memberExpression.Member.Name; + string propertyName = memberExpression.Member.Name; - Func accessor = property.Compile(); + Func accessor = property.Compile(); - return source.GetPropertyChangedEvents(scheduler) - .Where(x => x.EventArgs.PropertyName == propertyName) - .Select(_ => accessor(source)) - .StartWith(accessor(source)); - } + return source.GetPropertyChangedEvents(scheduler) + .Where(x => x.EventArgs.PropertyName == propertyName) + .Select(_ => accessor(source)) + .StartWith(accessor(source)); + } - // This is a wrapper around FromEvent(PropertyChanged) - public static IObservable> GetPropertyChangedEvents( - this INotifyPropertyChanged source, - IScheduler? scheduler = null) - { - return - Observable.FromEventPattern( - h => new PropertyChangedEventHandler(h), - h => source.PropertyChanged += h, - h => source.PropertyChanged -= h, - scheduler ?? Scheduler.Default); - } + // This is a wrapper around FromEvent(PropertyChanged) + public static IObservable> GetPropertyChangedEvents( + this INotifyPropertyChanged source, + IScheduler? scheduler = null) + { + return + Observable.FromEventPattern( + h => new PropertyChangedEventHandler(h), + h => source.PropertyChanged += h, + h => source.PropertyChanged -= h, + scheduler ?? Scheduler.Default); + } - public static IDisposable - Subscribe( + public static IDisposable + Subscribe( this TSource source, Expression> property, Action observer, IScheduler? scheduler = null) - where TSource : INotifyPropertyChanged - { - return source - .GetPropertyValues(property, scheduler) - .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe(observer); - } + where TSource : INotifyPropertyChanged + { + return source + .GetPropertyValues(property, scheduler) + .ObserveOn(Dispatcher.CurrentDispatcher) + .Subscribe(observer); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/Themes.cs b/src/Papercut.UI/Helpers/Themes.cs index 7b0d13c2..b617117c 100644 --- a/src/Papercut.UI/Helpers/Themes.cs +++ b/src/Papercut.UI/Helpers/Themes.cs @@ -16,32 +16,31 @@ // limitations under the License. -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public enum Themes { - public enum Themes - { - Red, - Green, - Blue, - Purple, - Orange, - Lime, - Emerald, - Teal, - Cyan, - Cobalt, - Indigo, - Violet, - Pink, - Magenta, - Crimson, - Amber, - Yellow, - Brown, - Olive, - Steel, - Mauve, - Taupe, - Sienna - } + Red, + Green, + Blue, + Purple, + Orange, + Lime, + Emerald, + Teal, + Cyan, + Cobalt, + Indigo, + Violet, + Pink, + Magenta, + Crimson, + Amber, + Yellow, + Brown, + Olive, + Steel, + Mauve, + Taupe, + Sienna } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/UIHelper.cs b/src/Papercut.UI/Helpers/UIHelper.cs index a227b6b9..eb537cd0 100644 --- a/src/Papercut.UI/Helpers/UIHelper.cs +++ b/src/Papercut.UI/Helpers/UIHelper.cs @@ -25,108 +25,107 @@ using MahApps.Metro.Controls; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public static class UIHelper { - public static class UIHelper - { - static readonly bool _isAeroEnabled; + static readonly bool _isAeroEnabled; - static UIHelper() + static UIHelper() + { + // figure out if Aero is enabled/disabled + try { - // figure out if Aero is enabled/disabled - try - { - DwmIsCompositionEnabled(out _isAeroEnabled); - } - catch - { - // ignored - } + DwmIsCompositionEnabled(out _isAeroEnabled); } - - [DllImport("dwmapi.dll")] - static extern IntPtr DwmIsCompositionEnabled(out bool pfEnabled); - - public static void AutoAdjustBorders(this MetroWindow window) + catch { - var appStyle = ThemeManager.Current.DetectTheme(Application.Current); + // ignored + } + } - // Only add borders if above call succeed and Aero Not enabled - var resourceDictionary = window.Resources; + [DllImport("dwmapi.dll")] + static extern IntPtr DwmIsCompositionEnabled(out bool pfEnabled); - if (appStyle != null) - SetBorder(resourceDictionary, appStyle.ShowcaseBrush); + public static void AutoAdjustBorders(this MetroWindow window) + { + var appStyle = ThemeManager.Current.DetectTheme(Application.Current); - ThemeManager.Current.ThemeChanged += (_, args) => - { - SetBorder(resourceDictionary, args.NewTheme.ShowcaseBrush); - }; - } + // Only add borders if above call succeed and Aero Not enabled + var resourceDictionary = window.Resources; + + if (appStyle != null) + SetBorder(resourceDictionary, appStyle.ShowcaseBrush); - private static void SetBorder(ResourceDictionary resourceDictionary, object brush) + ThemeManager.Current.ThemeChanged += (_, args) => { - TryRemoveFromResourceDictionary(resourceDictionary, "AccentBorderThickness"); - TryRemoveFromResourceDictionary(resourceDictionary, "AccentBorderBrush"); - TryRemoveFromResourceDictionary(resourceDictionary, "AccentGlowBrush"); + SetBorder(resourceDictionary, args.NewTheme.ShowcaseBrush); + }; + } - if (_isAeroEnabled) - { - resourceDictionary.Add("AccentBorderThickness", new Thickness(1)); - resourceDictionary.Add("AccentGlowBrush", brush); - } - else - { - resourceDictionary.Add("AccentBorderThickness", new Thickness(3)); - resourceDictionary.Add("AccentBorderBrush", brush); - } - } + private static void SetBorder(ResourceDictionary resourceDictionary, object brush) + { + TryRemoveFromResourceDictionary(resourceDictionary, "AccentBorderThickness"); + TryRemoveFromResourceDictionary(resourceDictionary, "AccentBorderBrush"); + TryRemoveFromResourceDictionary(resourceDictionary, "AccentGlowBrush"); - private static bool TryRemoveFromResourceDictionary(ResourceDictionary resourceDictionary, string keyName) + if (_isAeroEnabled) { - if (!resourceDictionary.Contains(keyName)) return false; + resourceDictionary.Add("AccentBorderThickness", new Thickness(1)); + resourceDictionary.Add("AccentGlowBrush", brush); + } + else + { + resourceDictionary.Add("AccentBorderThickness", new Thickness(3)); + resourceDictionary.Add("AccentBorderBrush", brush); + } + } - resourceDictionary.Remove(keyName); + private static bool TryRemoveFromResourceDictionary(ResourceDictionary resourceDictionary, string keyName) + { + if (!resourceDictionary.Contains(keyName)) return false; - return true; - } + resourceDictionary.Remove(keyName); - public static object? GetObjectDataFromPoint(this ListBox source, Point point) - { - ArgumentNullException.ThrowIfNull(source, nameof(source)); + return true; + } - if (source.InputHitTest(point) is UIElement element) - { - object data = DependencyProperty.UnsetValue; + public static object? GetObjectDataFromPoint(this ListBox source, Point point) + { + ArgumentNullException.ThrowIfNull(source, nameof(source)); - while (data == DependencyProperty.UnsetValue) - { - // Try to get the object value for the corresponding element - data = source.ItemContainerGenerator.ItemFromContainer(element); + if (source.InputHitTest(point) is UIElement element) + { + object data = DependencyProperty.UnsetValue; - // Get the parent and we will iterate again - if (data == DependencyProperty.UnsetValue && element != null) - element = VisualTreeHelper.GetParent(element) as UIElement; + while (data == DependencyProperty.UnsetValue) + { + // Try to get the object value for the corresponding element + data = source.ItemContainerGenerator.ItemFromContainer(element); - // If we reach the actual listbox then we must break to avoid an infinite loop - if (Equals(element, source)) return null; - } + // Get the parent and we will iterate again + if (data == DependencyProperty.UnsetValue && element != null) + element = VisualTreeHelper.GetParent(element) as UIElement; - return data; + // If we reach the actual listbox then we must break to avoid an infinite loop + if (Equals(element, source)) return null; } - // Get the object from the element - return null; + return data; } - public static T? FindAncestor(this DependencyObject dependencyObject) - where T : DependencyObject - { - ArgumentNullException.ThrowIfNull(dependencyObject); + // Get the object from the element + return null; + } - var parent = VisualTreeHelper.GetParent(dependencyObject); - if (parent == null) return null; - var parentT = parent as T; - return parentT ?? FindAncestor(parent); - } + public static T? FindAncestor(this DependencyObject dependencyObject) + where T : DependencyObject + { + ArgumentNullException.ThrowIfNull(dependencyObject); + + var parent = VisualTreeHelper.GetParent(dependencyObject); + if (parent == null) return null; + var parentT = parent as T; + return parentT ?? FindAncestor(parent); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/ViewModelWindowManager.cs b/src/Papercut.UI/Helpers/ViewModelWindowManager.cs index 969e6a9a..99ae97ed 100644 --- a/src/Papercut.UI/Helpers/ViewModelWindowManager.cs +++ b/src/Papercut.UI/Helpers/ViewModelWindowManager.cs @@ -20,38 +20,37 @@ using Caliburn.Micro; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public class ViewModelWindowManager(ILifetimeScope lifetimeScope) : WindowManager, IViewModelWindowManager { - public class ViewModelWindowManager(ILifetimeScope lifetimeScope) : WindowManager, IViewModelWindowManager + public async Task ShowDialogWithViewModel( + Action? setViewModel = null, + object? context = null) + where TViewModel : PropertyChangedBase { - public async Task ShowDialogWithViewModel( - Action? setViewModel = null, - object? context = null) - where TViewModel : PropertyChangedBase - { - var viewModel = lifetimeScope.Resolve(); - setViewModel?.Invoke(viewModel); - return await this.ShowDialogAsync(viewModel, context); - } + var viewModel = lifetimeScope.Resolve(); + setViewModel?.Invoke(viewModel); + return await this.ShowDialogAsync(viewModel, context); + } - public async Task ShowWindowWithViewModelAsync( - Action? setViewModel = null, - object? context = null) - where TViewModel : PropertyChangedBase - { - var viewModel = lifetimeScope.Resolve(); - setViewModel?.Invoke(viewModel); - await this.ShowWindowAsync(viewModel, context); - } + public async Task ShowWindowWithViewModelAsync( + Action? setViewModel = null, + object? context = null) + where TViewModel : PropertyChangedBase + { + var viewModel = lifetimeScope.Resolve(); + setViewModel?.Invoke(viewModel); + await this.ShowWindowAsync(viewModel, context); + } - public async Task ShowPopupWithViewModel( - Action? setViewModel = null, - object? context = null) - where TViewModel : PropertyChangedBase - { - var viewModel = lifetimeScope.Resolve(); - setViewModel?.Invoke(viewModel); - await this.ShowPopupAsync(viewModel, context); - } + public async Task ShowPopupWithViewModel( + Action? setViewModel = null, + object? context = null) + where TViewModel : PropertyChangedBase + { + var viewModel = lifetimeScope.Resolve(); + setViewModel?.Invoke(viewModel); + await this.ShowPopupAsync(viewModel, context); } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/WPFValueConverters.cs b/src/Papercut.UI/Helpers/WPFValueConverters.cs index 24a09333..b27d040b 100644 --- a/src/Papercut.UI/Helpers/WPFValueConverters.cs +++ b/src/Papercut.UI/Helpers/WPFValueConverters.cs @@ -20,86 +20,85 @@ using System.Windows; using System.Windows.Data; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +/// +/// Source: http://stackoverflow.com/a/1039681 +/// +[ValueConversion(typeof(bool), typeof(bool))] +public class InverseBooleanConverter : IValueConverter +{ + #region IValueConverter Members + + public object Convert( + object value, + Type targetType, + object parameter, + CultureInfo culture) + { + if (targetType != typeof(bool)) throw new InvalidOperationException("The target must be a boolean"); + + return !(bool)value; + } + + public object ConvertBack( + object value, + Type targetType, + object parameter, + CultureInfo culture) + { + throw new NotSupportedException(); + } + + #endregion +} + +/// +/// Converts Boolean Values to Control.Visibility values. +/// Source: http://www.codeproject.com/Tips/285358/All-purpose-Boolean-to-Visibility-Converter +/// +public class BooleanToVisibilityConverter : IValueConverter { - /// - /// Source: http://stackoverflow.com/a/1039681 - /// - [ValueConversion(typeof(bool), typeof(bool))] - public class InverseBooleanConverter : IValueConverter + //Set to true if you want to show control when boolean value is true + //Set to false if you want to hide/collapse control when value is true + + //Set to true if you just want to hide the control + //else set to false if you want to collapse the control + + public BooleanToVisibilityConverter() + { + this.TriggerValue = false; + } + + public bool TriggerValue { get; set; } + + public bool IsHidden { get; set; } + + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + bool booleanValue = value != null; + + if (value is string) booleanValue = !string.IsNullOrEmpty(value as string); + else if (value is bool) booleanValue = (bool)value; + + return this.GetVisibility(booleanValue); + } + + public object ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { - #region IValueConverter Members - - public object Convert( - object value, - Type targetType, - object parameter, - CultureInfo culture) - { - if (targetType != typeof(bool)) throw new InvalidOperationException("The target must be a boolean"); - - return !(bool)value; - } - - public object ConvertBack( - object value, - Type targetType, - object parameter, - CultureInfo culture) - { - throw new NotSupportedException(); - } - - #endregion + throw new NotImplementedException(); } - /// - /// Converts Boolean Values to Control.Visibility values. - /// Source: http://www.codeproject.com/Tips/285358/All-purpose-Boolean-to-Visibility-Converter - /// - public class BooleanToVisibilityConverter : IValueConverter + object GetVisibility(bool toggleValue) { - //Set to true if you want to show control when boolean value is true - //Set to false if you want to hide/collapse control when value is true - - //Set to true if you just want to hide the control - //else set to false if you want to collapse the control - - public BooleanToVisibilityConverter() - { - this.TriggerValue = false; - } - - public bool TriggerValue { get; set; } - - public bool IsHidden { get; set; } - - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool booleanValue = value != null; - - if (value is string) booleanValue = !string.IsNullOrEmpty(value as string); - else if (value is bool) booleanValue = (bool)value; - - return this.GetVisibility(booleanValue); - } - - public object ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - throw new NotImplementedException(); - } - - object GetVisibility(bool toggleValue) - { - if ((toggleValue && this.TriggerValue && this.IsHidden) - || (!toggleValue && !this.TriggerValue && this.IsHidden)) return Visibility.Hidden; - if ((toggleValue && this.TriggerValue && !this.IsHidden) - || (!toggleValue && !this.TriggerValue && !this.IsHidden)) return Visibility.Collapsed; - return Visibility.Visible; - } + if ((toggleValue && this.TriggerValue && this.IsHidden) + || (!toggleValue && !this.TriggerValue && this.IsHidden)) return Visibility.Hidden; + if ((toggleValue && this.TriggerValue && !this.IsHidden) + || (!toggleValue && !this.TriggerValue && !this.IsHidden)) return Visibility.Collapsed; + return Visibility.Visible; } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/WebView2Base.cs b/src/Papercut.UI/Helpers/WebView2Base.cs index 2462025d..9b310e6b 100644 --- a/src/Papercut.UI/Helpers/WebView2Base.cs +++ b/src/Papercut.UI/Helpers/WebView2Base.cs @@ -25,36 +25,35 @@ using Papercut.Core.Domain.Paths; using Papercut.Properties; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public class WebView2Base : WebView2 { - public class WebView2Base : WebView2 + public WebView2Base() { - public WebView2Base() - { - var webViewUserDataFolder = PathTemplateHelper.RenderPathTemplate(Settings.Default.WebView2UserFolder); + var webViewUserDataFolder = PathTemplateHelper.RenderPathTemplate(Settings.Default.WebView2UserFolder); - this.CreationProperties = new CoreWebView2CreationProperties() - { UserDataFolder = webViewUserDataFolder }; + this.CreationProperties = new CoreWebView2CreationProperties() + { UserDataFolder = webViewUserDataFolder }; - Log.Information("Setting WebView2 User Data Folder: {UserDataFolder}", webViewUserDataFolder); - } + Log.Information("Setting WebView2 User Data Folder: {UserDataFolder}", webViewUserDataFolder); + } - protected override void DestroyWindowCore(HandleRef hwnd) - { - this.SetVisible(false); + protected override void DestroyWindowCore(HandleRef hwnd) + { + this.SetVisible(false); - base.DestroyWindowCore(hwnd); - } + base.DestroyWindowCore(hwnd); + } - public void SetVisible(bool visible) + public void SetVisible(bool visible) + { + if (typeof(WebView2).GetField( + "_coreWebView2Controller", + BindingFlags.Instance | BindingFlags.NonPublic)? + .GetValue(this) is CoreWebView2Controller controller) { - if (typeof(WebView2).GetField( - "_coreWebView2Controller", - BindingFlags.Instance | BindingFlags.NonPublic)? - .GetValue(this) is CoreWebView2Controller controller) - { - controller.IsVisible = visible; - } + controller.IsVisible = visible; } } } \ No newline at end of file diff --git a/src/Papercut.UI/Helpers/WireupLogBridge.cs b/src/Papercut.UI/Helpers/WireupLogBridge.cs index baa71fa8..88bc711d 100644 --- a/src/Papercut.UI/Helpers/WireupLogBridge.cs +++ b/src/Papercut.UI/Helpers/WireupLogBridge.cs @@ -20,14 +20,13 @@ using Caliburn.Micro; -namespace Papercut.Helpers +namespace Papercut.Helpers; + +public class WireupLogBridge : IStartable { - public class WireupLogBridge : IStartable + public void Start() { - public void Start() - { - LogManager.GetLog = - type => new CalburnSerilogBridge(new Lazy(() => Log.ForContext(type))); - } + LogManager.GetLog = + type => new CalburnSerilogBridge(new Lazy(() => Log.ForContext(type))); } } \ No newline at end of file diff --git a/src/Papercut.UI/Infrastructure/Container/AutofacServiceProvider.cs b/src/Papercut.UI/Infrastructure/Container/AutofacServiceProvider.cs index 1f49472e..c7f73a77 100644 --- a/src/Papercut.UI/Infrastructure/Container/AutofacServiceProvider.cs +++ b/src/Papercut.UI/Infrastructure/Container/AutofacServiceProvider.cs @@ -18,29 +18,28 @@ using Autofac; -namespace Papercut.Infrastructure.Container -{ - public class AutofacServiceProvider : IServiceProvider - { - readonly ILifetimeScope _lifetimeScope; +namespace Papercut.Infrastructure.Container; - public AutofacServiceProvider(ILifetimeScope lifetimeScope) - { - this._lifetimeScope = lifetimeScope; - } +public class AutofacServiceProvider : IServiceProvider +{ + readonly ILifetimeScope _lifetimeScope; - public object GetService(Type serviceType) - { - return this._lifetimeScope.Resolve(serviceType); - } + public AutofacServiceProvider(ILifetimeScope lifetimeScope) + { + this._lifetimeScope = lifetimeScope; + } - #region Begin Static Container Registrations + public object GetService(Type serviceType) + { + return this._lifetimeScope.Resolve(serviceType); + } - static void Register(ContainerBuilder builder) - { - builder.RegisterType().As(); - } + #region Begin Static Container Registrations - #endregion + static void Register(ContainerBuilder builder) + { + builder.RegisterType().As(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/Infrastructure/LifecycleHooks/LifecycleHookHelpers.cs b/src/Papercut.UI/Infrastructure/LifecycleHooks/LifecycleHookHelpers.cs index 9acfac34..ba4e5761 100644 --- a/src/Papercut.UI/Infrastructure/LifecycleHooks/LifecycleHookHelpers.cs +++ b/src/Papercut.UI/Infrastructure/LifecycleHooks/LifecycleHookHelpers.cs @@ -21,63 +21,62 @@ using Papercut.Common.Extensions; using Papercut.Domain.LifecycleHooks; -namespace Papercut.Infrastructure.LifecycleHooks +namespace Papercut.Infrastructure.LifecycleHooks; + +public static class LifecycleHookHelpers { - public static class LifecycleHookHelpers + public static async Task RunLifecycleHooks(this IComponentContext container, Func> runHook) + where TLifecycle : IAppLifecycleHook { - public static async Task RunLifecycleHooks(this IComponentContext container, Func> runHook) - where TLifecycle : IAppLifecycleHook - { - ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(container); - var logger = container.Resolve(); + var logger = container.Resolve(); - foreach (var appLifecycleHook in container.Resolve>().MaybeByOrderable()) - { - logger.Debug("Running {LifecycleHookType}...", appLifecycleHook.GetType().FullName); + foreach (var appLifecycleHook in container.Resolve>().MaybeByOrderable()) + { + logger.Debug("Running {LifecycleHookType}...", appLifecycleHook.GetType().FullName); - var result = await runHook(appLifecycleHook); + var result = await runHook(appLifecycleHook); - if (result == AppLifecycleActionResultType.Cancel) - { - logger.Debug( - "{LifecycleHookType} has cancelled action {TLifecycle}", - appLifecycleHook.GetType().FullName, - typeof(TLifecycle)); + if (result == AppLifecycleActionResultType.Cancel) + { + logger.Debug( + "{LifecycleHookType} has cancelled action {TLifecycle}", + appLifecycleHook.GetType().FullName, + typeof(TLifecycle)); - return AppLifecycleActionResultType.Cancel; - } + return AppLifecycleActionResultType.Cancel; } - - return AppLifecycleActionResultType.Continue; } - public static async Task RunPreExit(this IComponentContext container) - { - ArgumentNullException.ThrowIfNull(container); + return AppLifecycleActionResultType.Continue; + } - return await container.RunLifecycleHooks(hook => hook.OnPreExit()); - } + public static async Task RunPreExit(this IComponentContext container) + { + ArgumentNullException.ThrowIfNull(container); - public static async Task RunPreStart(this IComponentContext container) - { - ArgumentNullException.ThrowIfNull(container); + return await container.RunLifecycleHooks(hook => hook.OnPreExit()); + } - return await container.RunLifecycleHooks(hook => hook.OnPreStart()); - } + public static async Task RunPreStart(this IComponentContext container) + { + ArgumentNullException.ThrowIfNull(container); - public static async Task RunStarted(this IComponentContext container) - { - ArgumentNullException.ThrowIfNull(container); + return await container.RunLifecycleHooks(hook => hook.OnPreStart()); + } - var logger = container.Resolve(); + public static async Task RunStarted(this IComponentContext container) + { + ArgumentNullException.ThrowIfNull(container); - foreach (var appLifecycleHook in container.Resolve>().MaybeByOrderable()) - { - logger.Debug("Running {LifecycleHookType}...", appLifecycleHook.GetType().FullName); + var logger = container.Resolve(); - await appLifecycleHook.OnStartedAsync(); - } + foreach (var appLifecycleHook in container.Resolve>().MaybeByOrderable()) + { + logger.Debug("Running {LifecycleHookType}...", appLifecycleHook.GetType().FullName); + + await appLifecycleHook.OnStartedAsync(); } } } \ No newline at end of file diff --git a/src/Papercut.UI/Infrastructure/Resources/AppResourceLocator.cs b/src/Papercut.UI/Infrastructure/Resources/AppResourceLocator.cs index ffd6161c..b1b9d59e 100644 --- a/src/Papercut.UI/Infrastructure/Resources/AppResourceLocator.cs +++ b/src/Papercut.UI/Infrastructure/Resources/AppResourceLocator.cs @@ -23,75 +23,74 @@ using Autofac; -namespace Papercut.Infrastructure.Resources +namespace Papercut.Infrastructure.Resources; + +public class AppResourceLocator { - public class AppResourceLocator + readonly string _appExecutableName; + + readonly ILogger _logger; + + public AppResourceLocator(ILogger logger) { - readonly string _appExecutableName; + this._logger = logger.ForContext(); + this._appExecutableName = + Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location); + } - readonly ILogger _logger; + public string GetResourceString(string resourceName) + { + ArgumentNullException.ThrowIfNull(resourceName); - public AppResourceLocator(ILogger logger) - { - this._logger = logger.ForContext(); - this._appExecutableName = - Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location); - } + var resource = + Assembly.GetExecutingAssembly() + .GetManifestResourceNames() + .FirstOrDefault(s => s.EndsWith(resourceName, StringComparison.OrdinalIgnoreCase)); - public string GetResourceString(string resourceName) + using ( + var streamReader = new StreamReader( + Assembly.GetExecutingAssembly().GetManifestResourceStream(resource), + Encoding.Default)) { - ArgumentNullException.ThrowIfNull(resourceName); - - var resource = - Assembly.GetExecutingAssembly() - .GetManifestResourceNames() - .FirstOrDefault(s => s.EndsWith(resourceName, StringComparison.OrdinalIgnoreCase)); - - using ( - var streamReader = new StreamReader( - Assembly.GetExecutingAssembly().GetManifestResourceStream(resource), - Encoding.Default)) - { - return streamReader.ReadToEnd(); - } + return streamReader.ReadToEnd(); } + } - public StreamResourceInfo GetResource(string resourceName) + public StreamResourceInfo GetResource(string resourceName) + { + try { - try - { - return - Application.GetResourceStream( - new Uri( - $"/{this._appExecutableName};component/{resourceName}", - UriKind.Relative)); - } - catch (Exception ex) - { - this._logger.Error( - ex, - "Failure loading application resource {ResourceName} in {ExecutableName}", - resourceName, - this._appExecutableName); - - throw; - } + return + Application.GetResourceStream( + new Uri( + $"/{this._appExecutableName};component/{resourceName}", + UriKind.Relative)); } - - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) + catch (Exception ex) { - ArgumentNullException.ThrowIfNull(builder); + this._logger.Error( + ex, + "Failure loading application resource {ResourceName} in {ExecutableName}", + resourceName, + this._appExecutableName); - builder.RegisterType().AsSelf().InstancePerLifetimeScope(); + throw; } + } - #endregion + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.RegisterType().AsSelf().InstancePerLifetimeScope(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/Infrastructure/Themes/ThemeColorRepository.cs b/src/Papercut.UI/Infrastructure/Themes/ThemeColorRepository.cs index 107db4bf..f3984f9b 100644 --- a/src/Papercut.UI/Infrastructure/Themes/ThemeColorRepository.cs +++ b/src/Papercut.UI/Infrastructure/Themes/ThemeColorRepository.cs @@ -22,39 +22,38 @@ using Papercut.Domain.Themes; -namespace Papercut.Infrastructure.Themes +namespace Papercut.Infrastructure.Themes; + +public class ThemeColorRepository { - public class ThemeColorRepository + private static List ThemeColors { get; } = typeof(Colors) + .GetProperties() + .Where(s => !s.Name.Equals("Transparent")) + .Select(p => new ThemeColor(p.Name, (Color)p.GetValue(null)!)) + .ToList(); + + public IReadOnlyCollection GetAll() => ThemeColors; + + public ThemeColor? FirstOrDefaultByName(string name) { - private static List ThemeColors { get; } = typeof(Colors) - .GetProperties() - .Where(s => !s.Name.Equals("Transparent")) - .Select(p => new ThemeColor(p.Name, (Color)p.GetValue(null)!)) - .ToList(); - - public IReadOnlyCollection GetAll() => ThemeColors; - - public ThemeColor? FirstOrDefaultByName(string name) - { - return this.GetAll().FirstOrDefault( - s => s.Name.Equals(name, StringComparison.OrdinalIgnoreCase) - || s.Description.Equals(name, StringComparison.OrdinalIgnoreCase)); - } - - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - builder.RegisterType().AsSelf().InstancePerLifetimeScope(); - } - - #endregion + return this.GetAll().FirstOrDefault( + s => s.Name.Equals(name, StringComparison.OrdinalIgnoreCase) + || s.Description.Equals(name, StringComparison.OrdinalIgnoreCase)); } + + #region Begin Static Container Registrations + + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.RegisterType().AsSelf().InstancePerLifetimeScope(); + } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/Infrastructure/WebView/WebView2Information.cs b/src/Papercut.UI/Infrastructure/WebView/WebView2Information.cs index 321757ca..d1ec0489 100644 --- a/src/Papercut.UI/Infrastructure/WebView/WebView2Information.cs +++ b/src/Papercut.UI/Infrastructure/WebView/WebView2Information.cs @@ -24,107 +24,106 @@ using Microsoft.Web.WebView2.Core; -namespace Papercut.Infrastructure.WebView +namespace Papercut.Infrastructure.WebView; + +public enum WebView2InstallType { - public enum WebView2InstallType - { - WebView2, EdgeChromiumBeta, EdgeChromiumCanary, EdgeChromiumDev, NotInstalled - } + WebView2, EdgeChromiumBeta, EdgeChromiumCanary, EdgeChromiumDev, NotInstalled +} + +/// +/// Code from https://github.com/mortenbrudvik/KioskBrowser/blob/main/src/KioskBrowser/WebView2Install.cs +/// THANK YOU +/// +public class WebView2Information +{ + #region Constructors and Destructors - /// - /// Code from https://github.com/mortenbrudvik/KioskBrowser/blob/main/src/KioskBrowser/WebView2Install.cs - /// THANK YOU - /// - public class WebView2Information + static WebView2Information() { - #region Constructors and Destructors + var loaderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? "", $@"runtimes\win-{RuntimeInformation.ProcessArchitecture}\native"); + + Log.Information("Setting WebView2 Loader Path to {LoaderPath}", loaderPath); - static WebView2Information() + try { - var loaderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? "", $@"runtimes\win-{RuntimeInformation.ProcessArchitecture}\native"); - - Log.Information("Setting WebView2 Loader Path to {LoaderPath}", loaderPath); - - try - { - CoreWebView2Environment.SetLoaderDllFolderPath(loaderPath); - } - catch (Exception ex) - { - Log.Error(ex, "Failure Settings WebView2 Loader Path"); - } + CoreWebView2Environment.SetLoaderDllFolderPath(loaderPath); } - - public WebView2Information() + catch (Exception ex) { - this.Version = this.GetWebView2Version(); - this.InstallType = this.GetInstallType(this.Version); + Log.Error(ex, "Failure Settings WebView2 Loader Path"); } + } - #endregion + public WebView2Information() + { + this.Version = this.GetWebView2Version(); + this.InstallType = this.GetInstallType(this.Version); + } - #region Public Properties + #endregion - public WebView2InstallType InstallType { get; } + #region Public Properties - public bool IsInstalled => this.InstallType != WebView2InstallType.NotInstalled; + public WebView2InstallType InstallType { get; } - public string Version { get; } + public bool IsInstalled => this.InstallType != WebView2InstallType.NotInstalled; - public Exception WebView2LoadException { get; private set; } + public string Version { get; } - #endregion + public Exception WebView2LoadException { get; private set; } - #region Methods + #endregion - #region Begin Static Container Registrations + #region Methods - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsSelf().SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.RegisterType().AsSelf().SingleInstance(); + } - #endregion + #endregion - private WebView2InstallType GetInstallType(string version) + private WebView2InstallType GetInstallType(string version) + { + if (string.IsNullOrWhiteSpace(version)) { - if (string.IsNullOrWhiteSpace(version)) - { - return WebView2InstallType.NotInstalled; - } + return WebView2InstallType.NotInstalled; + } - if (version.Contains("dev")) - return WebView2InstallType.EdgeChromiumDev; + if (version.Contains("dev")) + return WebView2InstallType.EdgeChromiumDev; - if (version.Contains("beta")) - return WebView2InstallType.EdgeChromiumBeta; + if (version.Contains("beta")) + return WebView2InstallType.EdgeChromiumBeta; - if (version.Contains("canary")) - return WebView2InstallType.EdgeChromiumCanary; + if (version.Contains("canary")) + return WebView2InstallType.EdgeChromiumCanary; - return WebView2InstallType.WebView2; - } + return WebView2InstallType.WebView2; + } - private string GetWebView2Version() + private string GetWebView2Version() + { + try { - try - { - return CoreWebView2Environment.GetAvailableBrowserVersionString(); - } - catch (Exception ex) - { - this.WebView2LoadException = ex; - return ""; - } + return CoreWebView2Environment.GetAvailableBrowserVersionString(); + } + catch (Exception ex) + { + this.WebView2LoadException = ex; + return ""; } - - #endregion } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/Infrastructure/WebView/WebView2Reporter.cs b/src/Papercut.UI/Infrastructure/WebView/WebView2Reporter.cs index 7f2fa2e3..f719e2d7 100644 --- a/src/Papercut.UI/Infrastructure/WebView/WebView2Reporter.cs +++ b/src/Papercut.UI/Infrastructure/WebView/WebView2Reporter.cs @@ -18,53 +18,52 @@ using Autofac; -namespace Papercut.Infrastructure.WebView +namespace Papercut.Infrastructure.WebView; + +public class WebView2Reporter : IStartable { - public class WebView2Reporter : IStartable + private readonly Lazy _logger; + + private readonly WebView2Information _webView2Information; + + public WebView2Reporter(Lazy logger, WebView2Information webView2Information) { - private readonly Lazy _logger; + this._logger = logger; + this._webView2Information = webView2Information; + } - private readonly WebView2Information _webView2Information; + protected ILogger Logger => this._logger.Value; - public WebView2Reporter(Lazy logger, WebView2Information webView2Information) + public void Start() + { + if (!this._webView2Information.IsInstalled) { - this._logger = logger; - this._webView2Information = webView2Information; + this.Logger.Error( + this._webView2Information.WebView2LoadException, + "Failure Loading 'WebView2' or Required Component 'WebView2' is not installed. Visit this URL to download: https://go.microsoft.com/fwlink/p/?LinkId=2124703"); } - - protected ILogger Logger => this._logger.Value; - - public void Start() + else { - if (!this._webView2Information.IsInstalled) - { - this.Logger.Error( - this._webView2Information.WebView2LoadException, - "Failure Loading 'WebView2' or Required Component 'WebView2' is not installed. Visit this URL to download: https://go.microsoft.com/fwlink/p/?LinkId=2124703"); - } - else - { - this.Logger.Information( - "WebView2 Installed Version {WebView2:l} {WebView2InstallType}", - this._webView2Information.Version, - this._webView2Information.InstallType); - } + this.Logger.Information( + "WebView2 Installed Version {WebView2:l} {WebView2InstallType}", + this._webView2Information.Version, + this._webView2Information.InstallType); } + } - #region Begin Static Container Registrations - - /// - /// Called dynamically from the RegisterStaticMethods() call in the container module. - /// - /// - [UsedImplicitly] - static void Register(ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); + #region Begin Static Container Registrations - builder.RegisterType().AsImplementedInterfaces().SingleInstance(); - } + /// + /// Called dynamically from the RegisterStaticMethods() call in the container module. + /// + /// + [UsedImplicitly] + static void Register(ContainerBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); - #endregion + builder.RegisterType().AsImplementedInterfaces().SingleInstance(); } + + #endregion } \ No newline at end of file diff --git a/src/Papercut.UI/Papercut.csproj b/src/Papercut.UI/Papercut.csproj index 5f4353f3..3f68b60f 100644 --- a/src/Papercut.UI/Papercut.csproj +++ b/src/Papercut.UI/Papercut.csproj @@ -51,23 +51,23 @@ - + - + - - + + - - - - + + + + - - + + - + @@ -88,4 +88,8 @@ Settings.Designer.cs + + + + \ No newline at end of file diff --git a/src/Papercut.UI/PapercutUIModule.cs b/src/Papercut.UI/PapercutUIModule.cs index fad1c8ab..2badd69d 100644 --- a/src/Papercut.UI/PapercutUIModule.cs +++ b/src/Papercut.UI/PapercutUIModule.cs @@ -21,6 +21,8 @@ using Caliburn.Micro; +using Microsoft.Extensions.Logging; + using Papercut.Core; using Papercut.Core.Domain.Application; using Papercut.Core.Infrastructure.Container; @@ -30,6 +32,8 @@ using Papercut.Message; using Papercut.Rules; +using Velopack; + namespace Papercut { [PublicAPI] @@ -45,12 +49,12 @@ private IEnumerable GetPapercutServiceModules() protected override void Load(ContainerBuilder builder) { - foreach (var module in this.GetPapercutServiceModules()) + foreach (var module in GetPapercutServiceModules()) { builder.RegisterModule(module); } - this.RegisterUI(builder); + RegisterUI(builder); // message watcher is needed for watching builder.RegisterType().AsSelf().SingleInstance(); @@ -59,6 +63,10 @@ protected override void Load(ContainerBuilder builder) .As() .SingleInstance(); + builder.Register(c => + new UpdateManager(AppConstants.UpgradeUrl, + logger: c.ResolveOptional>())).AsSelf().SingleInstance(); + builder.RegisterType() .As() .As() @@ -74,7 +82,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().AsImplementedInterfaces().SingleInstance(); - builder.RegisterStaticMethods(this.ThisAssembly); + builder.RegisterStaticMethods(ThisAssembly); base.Load(builder); } @@ -82,7 +90,7 @@ protected override void Load(ContainerBuilder builder) void RegisterUI(ContainerBuilder builder) { // register view models - builder.RegisterAssemblyTypes(this.ThisAssembly) + builder.RegisterAssemblyTypes(ThisAssembly) .Where(type => type.Name.EndsWith("ViewModel")) .AsImplementedInterfaces() .AsSelf() @@ -90,7 +98,7 @@ void RegisterUI(ContainerBuilder builder) .InstancePerDependency(); // register views - builder.RegisterAssemblyTypes(this.ThisAssembly) + builder.RegisterAssemblyTypes(ThisAssembly) .Where(type => type.Name.EndsWith("View")) .AsImplementedInterfaces() .AsSelf() diff --git a/src/Papercut.UI/Readme.eml b/src/Papercut.UI/Readme.eml index 57b691ee..79355536 100644 --- a/src/Papercut.UI/Readme.eml +++ b/src/Papercut.UI/Readme.eml @@ -1,7 +1,7 @@ From: Papercut SMTP -Date: Wed, 15 May 2024 01:12:03 -0400 +Date: Wed, 09 Oct 2024 20:40:05 -0400 Subject: Welcome to Papercut SMTP! -Message-Id: +Message-Id: <7X64I0IEBOU4.K9K6YQE5HHNG2@jaben-razer> To: Papercut User MIME-Version: 1.0 Content-Type: text/html; charset=utf-8 @@ -15,7 +15,7 @@ li { margin-bottom: 5px; }

The Simple Desktop Email Receiver.

Release Notes

-

Papercut SMTP v7.0.0 [2024-05-10]

+

Papercut SMTP v7.0.0 [2024-05-17]

NOTE: Uninstall any existing Papercut SMTP installations BEFORE installing this new version.

diff --git a/src/Papercut.UI/ViewModels/ForwardViewModel.cs b/src/Papercut.UI/ViewModels/ForwardViewModel.cs index 3386c008..d0ea8afa 100644 --- a/src/Papercut.UI/ViewModels/ForwardViewModel.cs +++ b/src/Papercut.UI/ViewModels/ForwardViewModel.cs @@ -24,128 +24,127 @@ using Papercut.Core; using Papercut.Properties; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public class ForwardViewModel : Screen { - public class ForwardViewModel : Screen - { - static readonly Regex _emailRegex = - new Regex( - @"(\A(\s*)\Z)|(\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); + static readonly Regex _emailRegex = + new Regex( + @"(\A(\s*)\Z)|(\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); - string _from; + string _from; - bool _fromSetting; + bool _fromSetting; - string _server; + string _server; - string _to; + string _to; - string _windowTitle = "Forward Message"; + string _windowTitle = "Forward Message"; - public bool FromSetting + public bool FromSetting + { + get => this._fromSetting; + set { - get => this._fromSetting; - set - { - this._fromSetting = value; - this.NotifyOfPropertyChange(() => this.FromSetting); - } + this._fromSetting = value; + this.NotifyOfPropertyChange(() => this.FromSetting); } + } - public string WindowTitle + public string WindowTitle + { + get => this._windowTitle; + set { - get => this._windowTitle; - set - { - this._windowTitle = value; - this.NotifyOfPropertyChange(() => this.WindowTitle); - } + this._windowTitle = value; + this.NotifyOfPropertyChange(() => this.WindowTitle); } + } - public string Server + public string Server + { + get => this._server; + set { - get => this._server; - set - { - this._server = value; - this.NotifyOfPropertyChange(() => this.Server); - } + this._server = value; + this.NotifyOfPropertyChange(() => this.Server); } + } - public string To + public string To + { + get => this._to; + set { - get => this._to; - set - { - this._to = value; - this.NotifyOfPropertyChange(() => this.To); - } + this._to = value; + this.NotifyOfPropertyChange(() => this.To); } + } - public string From + public string From + { + get => this._from; + set { - get => this._from; - set - { - this._from = value; - this.NotifyOfPropertyChange(() => this.From); - } + this._from = value; + this.NotifyOfPropertyChange(() => this.From); } + } - void Load() - { - // Load previous settings - this.Server = Settings.Default.ForwardServer; - this.To = Settings.Default.ForwardTo; - this.From = Settings.Default.ForwardFrom; - } + void Load() + { + // Load previous settings + this.Server = Settings.Default.ForwardServer; + this.To = Settings.Default.ForwardTo; + this.From = Settings.Default.ForwardFrom; + } - public async Task Cancel() + public async Task Cancel() + { + await this.TryCloseAsync(false); + } + + protected override void OnViewLoaded(object view) + { + base.OnViewLoaded(view); + + if (this.FromSetting) this.Load(); + } + + public async Task Send() + { + if (string.IsNullOrEmpty(this.Server) || string.IsNullOrEmpty(this.From) + || string.IsNullOrEmpty(this.To)) { - await this.TryCloseAsync(false); + MessageBox.Show( + "All the text boxes are required, fill them in please.", + AppConstants.ApplicationName, + MessageBoxButton.OK, + MessageBoxImage.Warning); + return; } - protected override void OnViewLoaded(object view) + if (!_emailRegex.IsMatch(this.From) || !_emailRegex.IsMatch(this.To)) { - base.OnViewLoaded(view); - - if (this.FromSetting) this.Load(); + MessageBox.Show( + "You need to enter valid email addresses.", + AppConstants.ApplicationName, + MessageBoxButton.OK, + MessageBoxImage.Warning); + return; } - public async Task Send() + if (this.FromSetting) { - if (string.IsNullOrEmpty(this.Server) || string.IsNullOrEmpty(this.From) - || string.IsNullOrEmpty(this.To)) - { - MessageBox.Show( - "All the text boxes are required, fill them in please.", - AppConstants.ApplicationName, - MessageBoxButton.OK, - MessageBoxImage.Warning); - return; - } - - if (!_emailRegex.IsMatch(this.From) || !_emailRegex.IsMatch(this.To)) - { - MessageBox.Show( - "You need to enter valid email addresses.", - AppConstants.ApplicationName, - MessageBoxButton.OK, - MessageBoxImage.Warning); - return; - } - - if (this.FromSetting) - { - // Save settings for the next time - Settings.Default.ForwardServer = this.Server.Trim(); - Settings.Default.ForwardTo = this.To.Trim(); - Settings.Default.ForwardFrom = this.From.Trim(); - Settings.Default.Save(); - } - - await this.TryCloseAsync(true); + // Save settings for the next time + Settings.Default.ForwardServer = this.Server.Trim(); + Settings.Default.ForwardTo = this.To.Trim(); + Settings.Default.ForwardFrom = this.From.Trim(); + Settings.Default.Save(); } + + await this.TryCloseAsync(true); } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/IMessageDetailItem.cs b/src/Papercut.UI/ViewModels/IMessageDetailItem.cs index 0b15b5ac..258d6939 100644 --- a/src/Papercut.UI/ViewModels/IMessageDetailItem.cs +++ b/src/Papercut.UI/ViewModels/IMessageDetailItem.cs @@ -15,8 +15,6 @@ // See the License for the specific language governing permissions and // limitations under the License. +namespace Papercut.ViewModels; -namespace Papercut.ViewModels -{ - public interface IMessageDetailItem {} -} \ No newline at end of file +public interface IMessageDetailItem {} \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/MainViewModel.cs b/src/Papercut.UI/ViewModels/MainViewModel.cs index 808eab4c..37311adc 100644 --- a/src/Papercut.UI/ViewModels/MainViewModel.cs +++ b/src/Papercut.UI/ViewModels/MainViewModel.cs @@ -23,6 +23,7 @@ using System.Reactive.Threading.Tasks; using System.Reflection; using System.Windows; +using System.Windows.Forms; using System.Windows.Threading; using Caliburn.Micro; @@ -35,6 +36,7 @@ using Papercut.AppLayer.LogSinks; using Papercut.AppLayer.NewVersionCheck; using Papercut.AppLayer.Uris; +using Papercut.Common.Extensions; using Papercut.Core; using Papercut.Core.Domain.Network.Smtp; using Papercut.Core.Infrastructure.Async; @@ -45,574 +47,640 @@ using Papercut.Infrastructure.Resources; using Papercut.Infrastructure.WebView; using Papercut.Properties; +using Papercut.Rules.App.Forwarding; +using Papercut.Rules.App.Relaying; using Papercut.Rules.Domain.Forwarding; -using Papercut.Rules.Domain.Relaying; using Papercut.Views; using Serilog.Events; -namespace Papercut.ViewModels +using Velopack; + +using Application = System.Windows.Application; +using MessageBox = System.Windows.MessageBox; + +namespace Papercut.ViewModels; + +public class MainViewModel : Conductor, + IHandle { - public class MainViewModel : Conductor, - IHandle - { - const string WindowTitleDefault = AppConstants.ApplicationName; + const string WindowTitleDefault = AppConstants.ApplicationName; - private readonly IAppCommandHub _appCommandHub; + private readonly IAppCommandHub _appCommandHub; - readonly ForwardRuleDispatch _forwardRuleDispatch; + readonly ForwardRuleDispatch _forwardRuleDispatch; - readonly AppResourceLocator _resourceLocator; + readonly AppResourceLocator _resourceLocator; - private readonly IUiCommandHub _uiCommandHub; + private readonly IUiCommandHub _uiCommandHub; - private readonly INewVersionProvider _newVersionProvider; + private readonly INewVersionProvider _newVersionProvider; + private readonly ILogger _logger; - readonly UiLogSinkQueue _uiLogSinkQueue; + private readonly UpdateManager _updateManager; - readonly IViewModelWindowManager _viewModelWindowManager; + readonly UiLogSinkQueue _uiLogSinkQueue; - private readonly WebView2Information _webView2Information; + readonly IViewModelWindowManager _viewModelWindowManager; - bool _isDeactivated; + private readonly WebView2Information _webView2Information; - private bool _isDeleteAllConfirmOpen; + bool _isDeactivated; - bool _isLogOpen; + private bool _isDeleteAllConfirmOpen; - private string? _upgradeVersion; + bool _isLogOpen; - private bool _isWebViewInstalled; + private string? _upgradeVersion; - string _logText; + private bool _isWebViewInstalled; - MetroWindow? _window; + string _logText; - string _windowTitle = WindowTitleDefault; + UpdateInfo? _updateInfo; - public Deque CurrentLogHistory = new(); + MetroWindow? _window; - public MainViewModel( - IViewModelWindowManager viewModelWindowManager, - IAppCommandHub appCommandHub, - IUiCommandHub uiCommandHub, - INewVersionProvider newVersionProvider, - WebView2Information webView2Information, - ForwardRuleDispatch forwardRuleDispatch, - Func messageListViewModelFactory, - Func messageDetailViewModelFactory, - UiLogSinkQueue uiLogSinkQueue, - AppResourceLocator resourceLocator) - { - this._viewModelWindowManager = viewModelWindowManager; - this._appCommandHub = appCommandHub; - this._uiCommandHub = uiCommandHub; - this._newVersionProvider = newVersionProvider; - this._webView2Information = webView2Information; - this._forwardRuleDispatch = forwardRuleDispatch; + string _windowTitle = WindowTitleDefault; - this.MessageListViewModel = messageListViewModelFactory(); - this.MessageDetailViewModel = messageDetailViewModelFactory(); + public Deque CurrentLogHistory = new(); - this.MessageListViewModel.ConductWith(this); - this.MessageDetailViewModel.ConductWith(this); + public MainViewModel( + IViewModelWindowManager viewModelWindowManager, + IAppCommandHub appCommandHub, + IUiCommandHub uiCommandHub, + INewVersionProvider newVersionProvider, + ILogger logger, + UpdateManager updateManager, + WebView2Information webView2Information, + ForwardRuleDispatch forwardRuleDispatch, + Func messageListViewModelFactory, + Func messageDetailViewModelFactory, + UiLogSinkQueue uiLogSinkQueue, + AppResourceLocator resourceLocator) + { + this._viewModelWindowManager = viewModelWindowManager; + this._appCommandHub = appCommandHub; + this._uiCommandHub = uiCommandHub; + this._newVersionProvider = newVersionProvider; + this._logger = logger; + this._updateManager = updateManager; + this._webView2Information = webView2Information; + this._forwardRuleDispatch = forwardRuleDispatch; - this._uiLogSinkQueue = uiLogSinkQueue; - this._resourceLocator = resourceLocator; + this.MessageListViewModel = messageListViewModelFactory(); + this.MessageDetailViewModel = messageDetailViewModelFactory(); - this.LogText = webView2Information.IsInstalled - ? this._resourceLocator.GetResourceString("LogClientSink.html") - : ""; + this.MessageListViewModel.ConductWith(this); + this.MessageDetailViewModel.ConductWith(this); - this.IsWebViewInstalled = this._webView2Information.IsInstalled; + this._uiLogSinkQueue = uiLogSinkQueue; + this._resourceLocator = resourceLocator; - this.SetupObservables(); - } + this.LogText = webView2Information.IsInstalled + ? this._resourceLocator.GetResourceString("LogClientSink.html") + : ""; - public MessageListViewModel MessageListViewModel { get; } + this.IsWebViewInstalled = this._webView2Information.IsInstalled; - public MessageDetailViewModel MessageDetailViewModel { get; } + this.SetupObservables(); + } - public bool IsWebViewInstalled - { + public MessageListViewModel MessageListViewModel { get; } - get => this._isWebViewInstalled; + public MessageDetailViewModel MessageDetailViewModel { get; } - set - { - this._isWebViewInstalled = value; - this.NotifyOfPropertyChange(() => this.IsWebViewInstalled); - } + public bool IsWebViewInstalled + { + + get => this._isWebViewInstalled; + + set + { + this._isWebViewInstalled = value; + this.NotifyOfPropertyChange(() => this.IsWebViewInstalled); } + } - public string LogText + public string LogText + { + get => this._logText; + set { - get => this._logText; - set - { - this._logText = value; - this.NotifyOfPropertyChange(() => this.LogText); - } + this._logText = value; + this.NotifyOfPropertyChange(() => this.LogText); } + } - public bool IsDeactivated + public bool IsDeactivated + { + get => this._isDeactivated; + set { - get => this._isDeactivated; - set - { - this._isDeactivated = value; - this.NotifyOfPropertyChange(() => this.IsDeactivated); - } + this._isDeactivated = value; + this.NotifyOfPropertyChange(() => this.IsDeactivated); } + } - public string WindowTitle + public string WindowTitle + { + get => this._windowTitle; + set { - get => this._windowTitle; - set - { - this._windowTitle = value; - this.NotifyOfPropertyChange(() => this.WindowTitle); - } + this._windowTitle = value; + this.NotifyOfPropertyChange(() => this.WindowTitle); } + } - public string Version => $"{AppConstants.ApplicationName} v{this.GetVersion()}"; + public string Version => $"{AppConstants.ApplicationName} v{this.GetVersion()}"; - public string? UpgradeVersion + public string? UpgradeVersion + { + get => this._upgradeVersion; + set { - get => this._upgradeVersion; - set + if (this._upgradeVersion != value) { - if (this._upgradeVersion != value) - { - this._upgradeVersion = value; - this.NotifyOfPropertyChange(() => this.UpgradeVersion); - } + this._upgradeVersion = value; + this.NotifyOfPropertyChange(() => this.UpgradeVersion); } } + } - public bool IsLogOpen + public bool IsLogOpen + { + get => this._isLogOpen; + set { - get => this._isLogOpen; - set + if (this._isLogOpen != value) { - if (this._isLogOpen != value) - { - this._isLogOpen = value; - this.NotifyOfPropertyChange(() => this.IsLogOpen); - } + this._isLogOpen = value; + this.NotifyOfPropertyChange(() => this.IsLogOpen); } } + } - public bool IsDeleteAllConfirmOpen + public bool IsDeleteAllConfirmOpen + { + get => this._isDeleteAllConfirmOpen; + set { - get => this._isDeleteAllConfirmOpen; - set + if (this._isDeleteAllConfirmOpen != value) { - if (this._isDeleteAllConfirmOpen != value) - { - this._isDeleteAllConfirmOpen = value; - this.NotifyOfPropertyChange(() => this.IsDeleteAllConfirmOpen); - } + this._isDeleteAllConfirmOpen = value; + this.NotifyOfPropertyChange(() => this.IsDeleteAllConfirmOpen); } } + } - public Task HandleAsync(SmtpServerBindFailedEvent message, CancellationToken cancellationToken = default) - { - MessageBox.Show( - "Failed to start SMTP server listening. The IP and Port combination is in use by another program. To fix, change the server bindings in the options.", - "Failed"); + public Task HandleAsync(SmtpServerBindFailedEvent message, CancellationToken cancellationToken = default) + { + MessageBox.Show( + "Failed to start SMTP server listening. The IP and Port combination is in use by another program. To fix, change the server bindings in the options.", + "Failed"); - this.ShowOptions(); + this.ShowOptions(); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - string? GetVersion() - { - var productVersion = - FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; + string? GetVersion() + { + var productVersion = + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; - return productVersion?.Split('+').FirstOrDefault(); - } + return productVersion?.Split('+').FirstOrDefault(); + } - protected override async Task OnActivateAsync(CancellationToken cancellationToken) - { - this._newVersionProvider.GetLatestVersionAsync(cancellationToken).ToObservable() - .Subscribe( - updateInfo => + protected override async Task OnActivateAsync(CancellationToken cancellationToken) + { + this._newVersionProvider.GetLatestVersionAsync(cancellationToken).ToObservable() + .Subscribe( + updateInfo => + { + if (updateInfo != null) { - if (updateInfo != null) - { - this.UpgradeVersion = $"Upgrade available! Click here to upgrade to v{updateInfo.TargetFullRelease.Version}"; - } - else - { - this.UpgradeVersion = null; - } - }); - - await base.OnActivateAsync(cancellationToken); - } - - public Task ExecuteAsync(ShowMainWindowCommand command, CancellationToken cancellationToken = default) - { - if (this._window == null) return Task.CompletedTask; + _updateInfo = updateInfo; + this.UpgradeVersion = $"Upgrade available! Click here to upgrade to v{updateInfo.TargetFullRelease.Version}"; + } + else + { + _updateInfo = null; + this.UpgradeVersion = null; + } + }); - if (!this._window.IsVisible) this._window.Show(); + await base.OnActivateAsync(cancellationToken); + } - if (this._window.WindowState == WindowState.Minimized) this._window.WindowState = WindowState.Normal; + public Task ExecuteAsync(ShowMainWindowCommand command, CancellationToken cancellationToken = default) + { + if (this._window == null) return Task.CompletedTask; - this._window.Activate(); + if (!this._window.IsVisible) this._window.Show(); - this._window.Topmost = true; - this._window.Topmost = false; + if (this._window.WindowState == WindowState.Minimized) this._window.WindowState = WindowState.Normal; - this._window.Focus(); + this._window.Activate(); - if (command.SelectMostRecentMessage) - { - this.MessageListViewModel.SelectMostRecentMessage(); - } + this._window.Topmost = true; + this._window.Topmost = false; - return Task.CompletedTask; - } + this._window.Focus(); - public async Task ExecuteAsync( - ShowMessageCommand message, - CancellationToken cancellationToken = default) + if (command.SelectMostRecentMessage) { - this.MessageDetailViewModel.IsLoading = true; - await this._window.ShowMessageAsync(message.Caption, message.MessageText); - this.MessageDetailViewModel.IsLoading = false; + this.MessageListViewModel.SelectMostRecentMessage(); } - public Task ExecuteAsync(ShowOptionWindowCommand message, CancellationToken cancellationToken = default) - { - this.ShowOptions(); + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public async Task ExecuteAsync( + ShowMessageCommand message, + CancellationToken cancellationToken = default) + { + this.MessageDetailViewModel.IsLoading = true; + await this._window.ShowMessageAsync(message.Caption, message.MessageText); + this.MessageDetailViewModel.IsLoading = false; + } - protected override void OnViewLoaded(object view) - { - base.OnViewLoaded(view); + public Task ExecuteAsync(ShowOptionWindowCommand message, CancellationToken cancellationToken = default) + { + this.ShowOptions(); - var typedView = view as MainView; + return Task.CompletedTask; + } - Debug.Assert(typedView != null, nameof(typedView) + " != null"); + protected override void OnViewLoaded(object view) + { + base.OnViewLoaded(view); - if (!this._webView2Information.IsInstalled) - { - this.GetPropertyValues(m => m.LogText) - .Throttle(TimeSpan.FromMilliseconds(200), TaskPoolScheduler.Default) - .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe(m => - { - typedView.LogPanelNoWebView.AppendText(m); - }); - } - else - { - typedView.LogPanel.CoreWebView2InitializationCompleted += (_, _) => - { - this.SetupWebView(typedView.LogPanel); - }; - } - } + var typedView = view as MainView; - private void SetupWebView(WebView2Base logPanel) - { - logPanel.CoreWebView2.DisableEdgeFeatures(); - logPanel.NavigateToString(this.GetLogSinkHtml()); + Debug.Assert(typedView != null, nameof(typedView) + " != null"); + if (!this._webView2Information.IsInstalled) + { this.GetPropertyValues(m => m.LogText) .Throttle(TimeSpan.FromMilliseconds(200), TaskPoolScheduler.Default) .ObserveOn(Dispatcher.CurrentDispatcher) .Subscribe(m => { - logPanel.NavigateToString(m); + typedView.LogPanelNoWebView.AppendText(m); }); } - - private string GetLogSinkHtml() + else { - return this._resourceLocator.GetResourceString("LogClientSink.html"); + typedView.LogPanel.CoreWebView2InitializationCompleted += (_, _) => + { + this.SetupWebView(typedView.LogPanel); + }; } + } - public IEnumerable RenderLogEventParts(LogEvent e) + private void SetupWebView(WebView2Base logPanel) + { + logPanel.CoreWebView2.DisableEdgeFeatures(); + logPanel.NavigateToString(this.GetLogSinkHtml()); + + this.GetPropertyValues(m => m.LogText) + .Throttle(TimeSpan.FromMilliseconds(200), TaskPoolScheduler.Default) + .ObserveOn(Dispatcher.CurrentDispatcher) + .Subscribe(m => + { + logPanel.NavigateToString(m); + }); + } + + private string GetLogSinkHtml() + { + return this._resourceLocator.GetResourceString("LogClientSink.html"); + } + + public IEnumerable RenderLogEventParts(LogEvent e) + { + if (!this._webView2Information.IsInstalled) { - if (!this._webView2Information.IsInstalled) + yield return $"[{e.Timestamp:G}][\"{e.Level}\"] {e.RenderMessage()}\r"; + if (e.Exception != null) { - yield return $"[{e.Timestamp:G}][\"{e.Level}\"] {e.RenderMessage()}\r"; - if (e.Exception != null) - { - yield return $"Exception: {e.Exception.Message}\r"; - } + yield return $"Exception: {e.Exception.Message}\r"; } - else + } + else + { + yield return $@"
"; + yield return $@"{e.Timestamp:G}"; + yield return $@"[{e.Level}]"; + yield return e.RenderMessage().Linkify(); + if (e.Exception != null) { - yield return $@"
"; - yield return $@"{e.Timestamp:G}"; - yield return $@"[{e.Level}]"; - yield return e.RenderMessage().Linkify(); - if (e.Exception != null) - { - yield return - $@"
Exception: {e.Exception.Message.Linkify()}"; - } - - yield return @"
"; + yield return + $@"
Exception: {e.Exception.Message.Linkify()}"; } + + yield return @"
"; } + } - void SetupObservables() - { - this.MessageListViewModel.GetPropertyValues(m => m.SelectedMessage) - .Throttle(TimeSpan.FromMilliseconds(200), TaskPoolScheduler.Default) - .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe( - _ => this.MessageDetailViewModel.LoadMessageEntry(this.MessageListViewModel.SelectedMessage)); - - Observable.FromEventPattern( - h => new EventHandler(h), - h => this._uiLogSinkQueue.LogEvent += h, - h => this._uiLogSinkQueue.LogEvent -= h, - TaskPoolScheduler.Default) - .Buffer(TimeSpan.FromSeconds(1)) // this will cause calling the Subscribe method every second. - .Select( - _ => - { - return - this._uiLogSinkQueue.GetLastEvents() - .Select(e => string.Join(" ", this.RenderLogEventParts(e))) - .ToList(); - }) - .Where(s => s.Any()) - .ObserveOn(Dispatcher.CurrentDispatcher).Subscribe( - o => - { - // If nothing added, return and don't process any data. And don't change LogText which would update the logs WebView2 component. - if(!o.Any()) { return; } + void SetupObservables() + { + this.MessageListViewModel.GetPropertyValues(m => m.SelectedMessage) + .Throttle(TimeSpan.FromMilliseconds(200), TaskPoolScheduler.Default) + .ObserveOn(Dispatcher.CurrentDispatcher) + .Subscribe( + _ => this.MessageDetailViewModel.LoadMessageEntry(this.MessageListViewModel.SelectedMessage)); + + Observable.FromEventPattern( + h => new EventHandler(h), + h => this._uiLogSinkQueue.LogEvent += h, + h => this._uiLogSinkQueue.LogEvent -= h, + TaskPoolScheduler.Default) + .Buffer(TimeSpan.FromSeconds(1)) // this will cause calling the Subscribe method every second. + .Select( + _ => + { + return + this._uiLogSinkQueue.GetLastEvents() + .Select(e => string.Join(" ", this.RenderLogEventParts(e))) + .ToList(); + }) + .Where(s => s.Any()) + .ObserveOn(Dispatcher.CurrentDispatcher).Subscribe( + o => + { + // If nothing added, return and don't process any data. And don't change LogText which would update the logs WebView2 component. + if(!o.Any()) { return; } + + foreach (var s in o) this.CurrentLogHistory.PushFront(s); - foreach (var s in o) this.CurrentLogHistory.PushFront(s); + if (this.CurrentLogHistory.Count > 150) + { + // prune + while (this.CurrentLogHistory.Count > 100) + { + this.CurrentLogHistory.PopBack(); + } - if (this.CurrentLogHistory.Count > 150) + if (!this._webView2Information.IsInstalled) { - // prune - while (this.CurrentLogHistory.Count > 100) - { - this.CurrentLogHistory.PopBack(); - } - - if (!this._webView2Information.IsInstalled) - { - var logItems = this.CurrentLogHistory.ToList(); - this.LogText = string.Join("", logItems); - } - else - { - // required pruning -- go ahead and replace the whole thing - var html = this.GetLogSinkHtml(); - var logItems = this.CurrentLogHistory.ToList(); - this.LogText = html.Replace( - "", - $"{string.Join("", logItems)}"); - } + var logItems = this.CurrentLogHistory.ToList(); + this.LogText = string.Join("", logItems); } else { - o.Reverse(); - - if (!this._webView2Information.IsInstalled) - { - this.LogText = string.Join("", o); - } - else - { - this.LogText = this.LogText.Replace( - "", - $"{string.Join("", o)}"); - } + // required pruning -- go ahead and replace the whole thing + var html = this.GetLogSinkHtml(); + var logItems = this.CurrentLogHistory.ToList(); + this.LogText = html.Replace( + "", + $"{string.Join("", logItems)}"); } - }); + } + else + { + o.Reverse(); - this.GetPropertyValues(m => m.IsLogOpen) - .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe(this.SetIsLoading); + if (!this._webView2Information.IsInstalled) + { + this.LogText = string.Join("", o); + } + else + { + this.LogText = this.LogText.Replace( + "", + $"{string.Join("", o)}"); + } + } + }); - this.GetPropertyValues(m => m.IsDeleteAllConfirmOpen) - .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe(this.SetIsLoading); + this.GetPropertyValues(m => m.IsLogOpen) + .ObserveOn(Dispatcher.CurrentDispatcher) + .Subscribe(this.SetIsLoading); - this._uiCommandHub.OnShowMainWindow.ObserveOn(Dispatcher.CurrentDispatcher) - .SubscribeAsync(async c => await this.ExecuteAsync(c)); + this.GetPropertyValues(m => m.IsDeleteAllConfirmOpen) + .ObserveOn(Dispatcher.CurrentDispatcher) + .Subscribe(this.SetIsLoading); - this._uiCommandHub.OnShowMessage.ObserveOn(Dispatcher.CurrentDispatcher) - .SubscribeAsync(async c => await this.ExecuteAsync(c)); + this._uiCommandHub.OnShowMainWindow.ObserveOn(Dispatcher.CurrentDispatcher) + .SubscribeAsync(async c => await this.ExecuteAsync(c)); - this._uiCommandHub.OnShowOptionWindow.ObserveOn(Dispatcher.CurrentDispatcher) - .SubscribeAsync(async c => await this.ExecuteAsync(c)); - } + this._uiCommandHub.OnShowMessage.ObserveOn(Dispatcher.CurrentDispatcher) + .SubscribeAsync(async c => await this.ExecuteAsync(c)); - public void GoToSite() + this._uiCommandHub.OnShowOptionWindow.ObserveOn(Dispatcher.CurrentDispatcher) + .SubscribeAsync(async c => await this.ExecuteAsync(c)); + } + + public async Task UpgradeToLatest() + { + if (this._updateInfo == null) { - new Uri("https://github.com/ChangemakerStudios/Papercut-SMTP").OpenUri(); + await this.ShowMessageAsync("Update Failure", "Missing Update Information."); + return; } - public void ShowRulesConfiguration() - { - this.CloseFlyouts(); + using var cancellationSource = new CancellationTokenSource(); - this._viewModelWindowManager.ShowDialogWithViewModel(); - } + var progressDialog = await this.ShowProgress("Updating", "Downloading Updates...", true, + cancellationSource); - public void DeleteAll() + try { - this.MessageListViewModel.DeleteAll(); - this.IsDeleteAllConfirmOpen = false; - } + // download new version + await this._updateManager.DownloadUpdatesAsync(this._updateInfo, cancelToken: cancellationSource.Token); - public void CancelDeleteAll() + await progressDialog.CloseAsync(); + + // install new version and restart app + this._updateManager.ApplyUpdatesAndRestart(this._updateInfo); + } + catch (Exception ex) { - this.IsDeleteAllConfirmOpen = false; + this._logger.Error(ex, "Update failed"); + + await progressDialog.CloseAsync(); + + await this.ShowMessageAsync("Update Failed", $"Failure during update: {ex.Message}"); } + } + + public void GoToSite() + { + new Uri("https://github.com/ChangemakerStudios/Papercut-SMTP").OpenUri(); + } + + public void ShowRulesConfiguration() + { + this.CloseFlyouts(); + + this._viewModelWindowManager.ShowDialogWithViewModel(); + } + + public void DeleteAll() + { + this.MessageListViewModel.DeleteAll(); + this.IsDeleteAllConfirmOpen = false; + } + + public void CancelDeleteAll() + { + this.IsDeleteAllConfirmOpen = false; + } + + public void ShowConfirmDeleteAll() + { + this.CloseFlyouts(); + this.IsDeleteAllConfirmOpen = true; + } - public void ShowConfirmDeleteAll() + public void ToggleLog() + { + if (!this.IsLogOpen) { this.CloseFlyouts(); - this.IsDeleteAllConfirmOpen = true; } - public void ToggleLog() - { - if (!this.IsLogOpen) - { - this.CloseFlyouts(); - } + this.IsLogOpen = !this.IsLogOpen; + } - this.IsLogOpen = !this.IsLogOpen; - } + public void ShowOptions() + { + this.CloseFlyouts(); - public void ShowOptions() - { - this.CloseFlyouts(); + this._viewModelWindowManager.ShowDialogWithViewModel(); + } - this._viewModelWindowManager.ShowDialogWithViewModel(); - } + private void CloseFlyouts() + { + this.IsLogOpen = false; + this.IsDeleteAllConfirmOpen = false; + } + + public void Exit() + { + this._appCommandHub.Shutdown(); + } + + private void SetIsLoading(bool isLoading) + { + this.MessageListViewModel.IsLoading = isLoading; + this.MessageDetailViewModel.IsLoading = isLoading; + } + + public async Task ShowProgress(string title, string message, bool allowCancellation = false, CancellationTokenSource? tokenSource = null) + { + this.SetIsLoading(true); - private void CloseFlyouts() + var progressDialog = await this._window.ShowProgressAsync(title, message); + + if (allowCancellation && tokenSource == null) { - this.IsLogOpen = false; - this.IsDeleteAllConfirmOpen = false; + throw new ArgumentNullException(nameof(tokenSource), + "If Allow Cancellation is true, Token Source must not be null"); } - public void Exit() + progressDialog.SetCancelable(allowCancellation); + progressDialog.SetIndeterminate(); + + progressDialog.Canceled += (_, _) => tokenSource?.Cancel(); + progressDialog.Closed += (_, _) => this.SetIsLoading(false); + + return progressDialog; + } + + public async Task ShowMessageAsync(string title, string message) + { + this.SetIsLoading(true); + + try { - this._appCommandHub.Shutdown(); + return await this._window.ShowMessageAsync(title, message); } - - private void SetIsLoading(bool isLoading) + finally { - this.MessageListViewModel.IsLoading = isLoading; - this.MessageDetailViewModel.IsLoading = isLoading; + this.SetIsLoading(false); } + } - public async Task ShowForwardingEmailProgress() - { - this.SetIsLoading(true); + public async Task ForwardSelected() + { + if (this.MessageListViewModel.SelectedMessage == null) return; - Task progressController = this._window.ShowProgressAsync("Forwarding Email...", "Please wait"); + var forwardViewModel = new ForwardViewModel {FromSetting = true}; + bool? result = await this._viewModelWindowManager.ShowDialogAsync(forwardViewModel); + if (result == null || !result.Value) return; - ProgressDialogController progressDialog = await progressController; + var progressDialog = await this.ShowProgress("Forwarding Email...", "Please wait"); - progressDialog.SetCancelable(false); - progressDialog.SetIndeterminate(); + try + { + var forwardRule = new ForwardRule + { + FromEmail = forwardViewModel.From, + ToEmail = forwardViewModel.To + }; - progressDialog.Closed += (_, _) => this.SetIsLoading(false); + forwardRule.PopulateServerFromUri(forwardViewModel.Server); - return progressDialog; + // send message using relay dispatcher... + await this._forwardRuleDispatch.DispatchAsync( + forwardRule, + this.MessageListViewModel.SelectedMessage); } - - public async Task ForwardSelected() + finally { - if (this.MessageListViewModel.SelectedMessage == null) return; - - var forwardViewModel = new ForwardViewModel {FromSetting = true}; - bool? result = await this._viewModelWindowManager.ShowDialogAsync(forwardViewModel); - if (result == null || !result.Value) return; - - var progressDialog = await this.ShowForwardingEmailProgress(); - - Observable.Start( - async () => - { - var forwardRule = new ForwardRule - { - FromEmail = forwardViewModel.From, - ToEmail = forwardViewModel.To - }; - - forwardRule.PopulateServerFromUri(forwardViewModel.Server); - - // send message using relay dispatcher... - await this._forwardRuleDispatch.DispatchAsync( - forwardRule, - this.MessageListViewModel.SelectedMessage); - - return true; - }, - TaskPoolScheduler.Default) - .ObserveOn(Dispatcher.CurrentDispatcher) - .SubscribeAsync(async _ => await progressDialog.CloseAsync()); + await progressDialog.CloseAsync(); } + } - protected override void OnViewAttached(object view, object context) - { - base.OnViewAttached(view, context); + protected override void OnViewAttached(object view, object context) + { + base.OnViewAttached(view, context); - this._window = view as MainView; + this._window = view as MainView; - if (this._window == null) return; + if (this._window == null) return; - //_window.Flyouts.FindChild("LogFlyouts") + //_window.Flyouts.FindChild("LogFlyouts") - this._window.StateChanged += (_, _) => + this._window.StateChanged += (_, _) => + { + if (this._window.WindowState == WindowState.Minimized && Settings.Default.MinimizeToTray) { - if (this._window.WindowState == WindowState.Minimized && Settings.Default.MinimizeToTray) - { - // Hide the window if minimized so it doesn't show up on the task bar - this._window.Hide(); - } - }; + // Hide the window if minimized so it doesn't show up on the task bar + this._window.Hide(); + } + }; - this._window.Closing += (_, args) => + this._window.Closing += (_, args) => + { + if (Application.Current.ShutdownMode == ShutdownMode.OnExplicitShutdown) return; + + // Cancel close and minimize if setting is set to minimize on close + if (Settings.Default.MinimizeOnClose) { - if (Application.Current.ShutdownMode == ShutdownMode.OnExplicitShutdown) return; + args.Cancel = true; + this._window.WindowState = WindowState.Minimized; + } + }; + + this._window.Activated += (_, _) => this.IsDeactivated = false; + this._window.Deactivated += (_, _) => this.IsDeactivated = true; - // Cancel close and minimize if setting is set to minimize on close - if (Settings.Default.MinimizeOnClose) + // Minimize if set to + if (Settings.Default.StartMinimized) + { + bool initialWindowActivate = true; + this._window.Activated += (_, _) => + { + if (initialWindowActivate) { - args.Cancel = true; + initialWindowActivate = false; this._window.WindowState = WindowState.Minimized; } }; - - this._window.Activated += (_, _) => this.IsDeactivated = false; - this._window.Deactivated += (_, _) => this.IsDeactivated = true; - - // Minimize if set to - if (Settings.Default.StartMinimized) - { - bool initialWindowActivate = true; - this._window.Activated += (_, _) => - { - if (initialWindowActivate) - { - initialWindowActivate = false; - this._window.WindowState = WindowState.Minimized; - } - }; - } } } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/MessageDetailBodyViewModel.cs b/src/Papercut.UI/ViewModels/MessageDetailBodyViewModel.cs index 77f52ece..d72c18c1 100644 --- a/src/Papercut.UI/ViewModels/MessageDetailBodyViewModel.cs +++ b/src/Papercut.UI/ViewModels/MessageDetailBodyViewModel.cs @@ -23,43 +23,42 @@ using Papercut.Helpers; using Papercut.Views; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public sealed class MessageDetailBodyViewModel : Screen, IMessageDetailItem { - public sealed class MessageDetailBodyViewModel : Screen, IMessageDetailItem - { - readonly ILogger _logger; + readonly ILogger _logger; - string? _body; + string? _body; - public MessageDetailBodyViewModel(ILogger logger) - { - this._logger = logger; - this.DisplayName = "Body"; - } + public MessageDetailBodyViewModel(ILogger logger) + { + this._logger = logger; + this.DisplayName = "Body"; + } - public string? Body + public string? Body + { + get => this._body; + set { - get => this._body; - set - { - this._body = value; - this.NotifyOfPropertyChange(() => this.Body); - } + this._body = value; + this.NotifyOfPropertyChange(() => this.Body); } + } - protected override void OnViewLoaded(object view) - { - base.OnViewLoaded(view); - - if (!(view is MessageDetailBodyView typedView)) - { - this._logger.Error("Unable to locate the MessageDetailBodyView to hook the Text Control"); - return; - } + protected override void OnViewLoaded(object view) + { + base.OnViewLoaded(view); - this.GetPropertyValues(p => p.Body) - .Subscribe( - t => { typedView.BodyEdit.Document = new TextDocument(new StringTextSource(t ?? string.Empty)); }); + if (!(view is MessageDetailBodyView typedView)) + { + this._logger.Error("Unable to locate the MessageDetailBodyView to hook the Text Control"); + return; } + + this.GetPropertyValues(p => p.Body) + .Subscribe( + t => { typedView.BodyEdit.Document = new TextDocument(new StringTextSource(t ?? string.Empty)); }); } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/MessageDetailHeaderViewModel.cs b/src/Papercut.UI/ViewModels/MessageDetailHeaderViewModel.cs index 36e05d19..7cc557b1 100644 --- a/src/Papercut.UI/ViewModels/MessageDetailHeaderViewModel.cs +++ b/src/Papercut.UI/ViewModels/MessageDetailHeaderViewModel.cs @@ -23,43 +23,42 @@ using Papercut.Helpers; using Papercut.Views; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public class MessageDetailHeaderViewModel : Screen, IMessageDetailItem { - public class MessageDetailHeaderViewModel : Screen, IMessageDetailItem - { - readonly ILogger _logger; + readonly ILogger _logger; - string? _headers; + string? _headers; - public MessageDetailHeaderViewModel(ILogger logger) - { - this._logger = logger; - this.DisplayName = "Headers"; - } + public MessageDetailHeaderViewModel(ILogger logger) + { + this._logger = logger; + this.DisplayName = "Headers"; + } - public string? Headers + public string? Headers + { + get => this._headers; + set { - get => this._headers; - set - { - this._headers = value; - this.NotifyOfPropertyChange(() => this.Headers); - } + this._headers = value; + this.NotifyOfPropertyChange(() => this.Headers); } + } - protected override void OnViewLoaded(object view) - { - base.OnViewLoaded(view); - - if (view is not MessageDetailHeaderView typedView) - { - this._logger.Error("Unable to locate the MessageDetailHeaderView to hook the Text Control"); - return; - } + protected override void OnViewLoaded(object view) + { + base.OnViewLoaded(view); - this.GetPropertyValues(p => p.Headers) - .Subscribe( - t => { typedView.HeaderEdit.Document = new TextDocument(new StringTextSource(t ?? string.Empty)); }); + if (view is not MessageDetailHeaderView typedView) + { + this._logger.Error("Unable to locate the MessageDetailHeaderView to hook the Text Control"); + return; } + + this.GetPropertyValues(p => p.Headers) + .Subscribe( + t => { typedView.HeaderEdit.Document = new TextDocument(new StringTextSource(t ?? string.Empty)); }); } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/MessageDetailHtmlViewModel.cs b/src/Papercut.UI/ViewModels/MessageDetailHtmlViewModel.cs index 60df8f37..d4d0c9a1 100644 --- a/src/Papercut.UI/ViewModels/MessageDetailHtmlViewModel.cs +++ b/src/Papercut.UI/ViewModels/MessageDetailHtmlViewModel.cs @@ -17,6 +17,7 @@ using System.Diagnostics; +using System.IO; using System.Reactive.Linq; using System.Windows; using System.Windows.Threading; @@ -31,193 +32,247 @@ using Papercut.Common.Extensions; using Papercut.Common.Helper; using Papercut.Core.Infrastructure.Async; +using Papercut.Core.Infrastructure.Logging; using Papercut.Domain.HtmlPreviews; using Papercut.Helpers; using Papercut.Infrastructure.WebView; using Papercut.Views; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public class MessageDetailHtmlViewModel : Screen, IMessageDetailItem { - public class MessageDetailHtmlViewModel : Screen, IMessageDetailItem - { - readonly ILogger _logger; + readonly ILogger _logger; + + readonly IHtmlPreviewGenerator _previewGenerator; - readonly IHtmlPreviewGenerator _previewGenerator; + private readonly WebView2Information _webView2Information; - private readonly WebView2Information _webView2Information; + private string? _htmlFile; - private string? _htmlFile; + private bool _isWebViewInstalled = false; - private bool _isWebViewInstalled = false; + public MessageDetailHtmlViewModel(ILogger logger, WebView2Information webView2Information, IHtmlPreviewGenerator previewGenerator) + { + this.DisplayName = "Message"; + this._logger = logger; + this._webView2Information = webView2Information; + this._previewGenerator = previewGenerator; + this.IsWebViewInstalled = this._webView2Information.IsInstalled; + } - public MessageDetailHtmlViewModel(ILogger logger, WebView2Information webView2Information, IHtmlPreviewGenerator previewGenerator) + public bool IsWebViewInstalled + { + + get => this._isWebViewInstalled; + + set { - this.DisplayName = "Message"; - this._logger = logger; - this._webView2Information = webView2Information; - this._previewGenerator = previewGenerator; - this.IsWebViewInstalled = this._webView2Information.IsInstalled; + this._isWebViewInstalled = value; + this.NotifyOfPropertyChange(() => this.IsWebViewInstalled); + this.NotifyOfPropertyChange(() => this.ShowHtmlView); } + } - public bool IsWebViewInstalled - { + public string? HtmlFile + { - get => this._isWebViewInstalled; + get => this._htmlFile; - set - { - this._isWebViewInstalled = value; - this.NotifyOfPropertyChange(() => this.IsWebViewInstalled); - this.NotifyOfPropertyChange(() => this.ShowHtmlView); - } + set + { + this._htmlFile = value; + this.NotifyOfPropertyChange(() => this.HtmlFile); + this.NotifyOfPropertyChange(() => this.ShowHtmlView); } + } + + public bool ShowHtmlView => !string.IsNullOrWhiteSpace(this.HtmlFile); - public string? HtmlFile + public void ShowMessage(MimeMessage? mailMessageEx) + { + ArgumentNullException.ThrowIfNull(mailMessageEx); + + try + { + this.HtmlFile = this._previewGenerator.GetHtmlPreviewFile(mailMessageEx); + } + catch (Exception ex) { + this._logger.Error(ex, "Failure Saving Browser Temp File for {MailMessage}", mailMessageEx.ToString()); + } + } - get => this._htmlFile; + private bool IsLocalNavigation(string navigateToUrl) + { + if (string.IsNullOrEmpty(navigateToUrl)) + { + return true; + } - set - { - this._htmlFile = value; - this.NotifyOfPropertyChange(() => this.HtmlFile); - this.NotifyOfPropertyChange(() => this.ShowHtmlView); - } + if (navigateToUrl.StartsWith("file:") || navigateToUrl.StartsWith("about:") || navigateToUrl.StartsWith("data:text/html")) + { + return true; } - public bool ShowHtmlView => !string.IsNullOrWhiteSpace(this.HtmlFile); + return false; + } + + protected override void OnViewLoaded(object view) + { + base.OnViewLoaded(view); - public void ShowMessage(MimeMessage? mailMessageEx) + if (view is not MessageDetailHtmlView typedView) { - ArgumentNullException.ThrowIfNull(mailMessageEx); + this._logger.Error("Unable to locate the MessageDetailHtmlView to hook the WebBrowser Control"); + return; + } - try + typedView.htmlView.CoreWebView2InitializationCompleted += (_, args) => + { + if (!args.IsSuccess) { - this.HtmlFile = this._previewGenerator.GetHtmlPreviewFile(mailMessageEx); + this._logger.Error( + args.InitializationException, + "Failure Initializing Edge WebView2"); + } - catch (Exception ex) + else { - this._logger.Error(ex, "Failure Saving Browser Temp File for {MailMessage}", mailMessageEx.ToString()); + this.SetupWebView(typedView.htmlView.CoreWebView2); } - } + }; - private bool ShouldNavigateToUrl(string navigateToUrl) + if (!typedView.IsEnabled) { - if (string.IsNullOrEmpty(navigateToUrl)) + typedView.htmlView.Visibility = Visibility.Collapsed; + } + + Observable + .FromEvent( + a => (_, e) => a(e), + h => typedView.IsEnabledChanged += h, + h => typedView.IsEnabledChanged -= h) + .Throttle(TimeSpan.FromMilliseconds(100)) + .Select(args => args.NewValue.ToType() + ? Visibility.Visible + : Visibility.Collapsed) + .ObserveOn(Dispatcher.CurrentDispatcher) + .Subscribe((newState) => { - return true; - } + if (typedView.htmlView.Visibility != newState) + { + typedView.htmlView.Visibility = newState; + } + }); + + typedView.htmlView.ContextMenuOpening += (_, args) => + { + args.Handled = true; + }; + } - if (navigateToUrl.StartsWith("file:") || navigateToUrl.StartsWith("about:") || navigateToUrl.StartsWith("data:text/html")) + private void SetupWebView(CoreWebView2 coreWebView) + { + coreWebView.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); + + var externalNavigation = new List(); + + coreWebView.WebResourceRequested += async (_, args) => + { + if (externalNavigation.Contains(args.Request.Uri)) { - return true; - } + // handle the response + var response = + coreWebView.Environment.CreateWebResourceResponse(new MemoryStream(), 200, "OK", + "Content-Type: text/html"); + args.Response = response; - return false; - } + externalNavigation.Remove(args.Request.Uri); + } + }; - protected override void OnViewLoaded(object view) + coreWebView.NewWindowRequested += async (_, args) => { - base.OnViewLoaded(view); + var internalUrl = !args.IsUserInitiated || this.IsLocalNavigation(args.Uri); - if (view is not MessageDetailHtmlView typedView) + if (internalUrl) { - this._logger.Error("Unable to locate the MessageDetailHtmlView to hook the WebBrowser Control"); + args.Handled = false; return; } - typedView.htmlView.CoreWebView2InitializationCompleted += (_, args) => - { - if (!args.IsSuccess) - { - this._logger.Error( - args.InitializationException, - "Failure Initializing Edge WebView2"); - - } - else - { - this.SetupWebView(typedView.htmlView.CoreWebView2); - } - }; + // external navigation + args.Handled = true; - void VisibilityChanged(DependencyPropertyChangedEventArgs o) + try { - typedView.htmlView.Visibility = o.NewValue.ToType() - ? Visibility.Visible - : Visibility.Collapsed; + await this.DoInternalNavigationAsync(new Uri(args.Uri)); } - - if (!typedView.IsEnabled) + catch (Exception ex) when (_logger.ErrorWithContext(ex, "Failure Navigating to External Url {Url}", args.Uri)) { - typedView.htmlView.Visibility = Visibility.Collapsed; } + }; - Observable.FromEvent( - a => (_, e) => a(e), - h => typedView.IsEnabledChanged += h, - h => typedView.IsEnabledChanged -= h) - .Throttle(TimeSpan.FromMilliseconds(100)) - .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe(VisibilityChanged); - - typedView.htmlView.ContextMenuOpening += (_, args) => - { - args.Handled = true; - }; + coreWebView.NavigationStarting += async (_, args) => + { + var uri = args.Uri; - } + var internalUrl = !args.IsUserInitiated || this.IsLocalNavigation(uri); - private void SetupWebView(CoreWebView2 coreWebView) - { - coreWebView.NavigationStarting += (_, args) => + if (internalUrl) { - var shouldNavigateToUrl = this.ShouldNavigateToUrl(args.Uri); + args.Cancel = false; + return; + } - if (shouldNavigateToUrl) - { - args.Cancel = false; - return; - } + externalNavigation.Add(uri); - // do internal navigation - args.Cancel = true; - this.DoInternalNavigationAsync(new Uri(args.Uri)).RunAsync(); - }; + // external navigation + args.Cancel = true; - coreWebView.DisableEdgeFeatures(); + try + { + await this.DoInternalNavigationAsync(new Uri(uri)); + } + catch (Exception ex) when (_logger.ErrorWithContext(ex, "Failure Navigating to External Url {Url}", uri)) + { + } + }; + + coreWebView.DisableEdgeFeatures(); - this.GetPropertyValues(p => p.HtmlFile) - .Subscribe( - file => + this.GetPropertyValues(p => p.HtmlFile) + .Subscribe( + file => + { + if (file.IsNullOrWhiteSpace()) { - if (file.IsNullOrWhiteSpace()) - { - coreWebView.NavigateToString(string.Empty); - } - else - { - coreWebView.Navigate($"file://{file.Replace("/", @"\")}"); - } + coreWebView.NavigateToString(string.Empty); } - ); - } + else + { + coreWebView.Navigate($"file://{file.Replace("/", @"\")}"); + } + } + ); + } - private async Task DoInternalNavigationAsync(Uri navigateToUri) + private async Task DoInternalNavigationAsync(Uri navigateToUri) + { + if (navigateToUri.Scheme == Uri.UriSchemeHttp || navigateToUri.Scheme == Uri.UriSchemeHttps) { - if (navigateToUri.Scheme == Uri.UriSchemeHttp || navigateToUri.Scheme == Uri.UriSchemeHttps) - { - navigateToUri.OpenUri(); - } - else if (navigateToUri.Scheme.Equals("cid", StringComparison.OrdinalIgnoreCase)) + navigateToUri.OpenUri(); + } + else if (navigateToUri.Scheme.Equals("cid", StringComparison.OrdinalIgnoreCase)) + { + // direct to the parts area... + var model = await this.GetConductor().ActivateViewModelOf(); + var part = model.Parts.FirstOrDefault(s => s.ContentId == navigateToUri.AbsolutePath); + if (part != null) { - // direct to the parts area... - var model = await this.GetConductor().ActivateViewModelOf(); - var part = model.Parts.FirstOrDefault(s => s.ContentId == navigateToUri.AbsolutePath); - if (part != null) - { - model.SelectedPart = part; - } + model.SelectedPart = part; } } } diff --git a/src/Papercut.UI/ViewModels/MessageDetailPartsListViewModel.cs b/src/Papercut.UI/ViewModels/MessageDetailPartsListViewModel.cs index a0442405..b999128a 100644 --- a/src/Papercut.UI/ViewModels/MessageDetailPartsListViewModel.cs +++ b/src/Papercut.UI/ViewModels/MessageDetailPartsListViewModel.cs @@ -33,187 +33,186 @@ using Papercut.Message; using Papercut.Message.Helpers; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public sealed class MessageDetailPartsListViewModel : Screen, IMessageDetailItem { - public sealed class MessageDetailPartsListViewModel : Screen, IMessageDetailItem - { - readonly ILogger _logger; + readonly ILogger _logger; - private readonly MessageRepository _messageRepository; + private readonly MessageRepository _messageRepository; - readonly IViewModelWindowManager _viewModelWindowManager; + readonly IViewModelWindowManager _viewModelWindowManager; - bool _hasSelectedPart; + bool _hasSelectedPart; - MimeMessage? _mimeMessage; + MimeMessage? _mimeMessage; - MimeEntity? _selectedPart; + MimeEntity? _selectedPart; - public MessageDetailPartsListViewModel(MessageRepository messageRepository, IViewModelWindowManager viewModelWindowManager, ILogger logger) - { - this.DisplayName = "Sections"; - this._messageRepository = messageRepository; - this._viewModelWindowManager = viewModelWindowManager; - this._logger = logger; - this.Parts = new ObservableCollection(); - } + public MessageDetailPartsListViewModel(MessageRepository messageRepository, IViewModelWindowManager viewModelWindowManager, ILogger logger) + { + this.DisplayName = "Sections"; + this._messageRepository = messageRepository; + this._viewModelWindowManager = viewModelWindowManager; + this._logger = logger; + this.Parts = new ObservableCollection(); + } - public ObservableCollection Parts { get; } + public ObservableCollection Parts { get; } - public MimeMessage? MimeMessage + public MimeMessage? MimeMessage + { + get => this._mimeMessage; + set { - get => this._mimeMessage; - set - { - this._mimeMessage = value; - this.NotifyOfPropertyChange(() => this.MimeMessage); + this._mimeMessage = value; + this.NotifyOfPropertyChange(() => this.MimeMessage); - if (this._mimeMessage != null) this.RefreshParts(); - } + if (this._mimeMessage != null) this.RefreshParts(); } + } - public MimeEntity? SelectedPart + public MimeEntity? SelectedPart + { + get => this._selectedPart; + set { - get => this._selectedPart; - set - { - this._selectedPart = value; - this.HasSelectedPart = this._selectedPart != null; - this.NotifyOfPropertyChange(() => this.SelectedPart); - } + this._selectedPart = value; + this.HasSelectedPart = this._selectedPart != null; + this.NotifyOfPropertyChange(() => this.SelectedPart); } + } - public bool HasSelectedPart + public bool HasSelectedPart + { + get => this._hasSelectedPart; + set { - get => this._hasSelectedPart; - set - { - this._hasSelectedPart = value; - this.NotifyOfPropertyChange(() => this.HasSelectedPart); - } + this._hasSelectedPart = value; + this.NotifyOfPropertyChange(() => this.HasSelectedPart); } + } + + public void ViewSection() + { + MimeEntity part = this.SelectedPart; - public void ViewSection() + if (part == null) { - MimeEntity part = this.SelectedPart; + return; + } - if (part == null) - { - return; - } + if (part is TextPart textPart) + { + // show in the viewer... + this._viewModelWindowManager.ShowDialogWithViewModel(vm => vm.PartText = textPart.Text); + } + else if (part is MessagePart messagePart) + { + this._viewModelWindowManager.ShowDialogWithViewModel(vm => vm.PartText = messagePart.Message.ToString()); + } + else if (part is MimePart mimePart) + { + string tempFileName; - if (part is TextPart textPart) + if (mimePart.FileName.IsSet()) { - // show in the viewer... - this._viewModelWindowManager.ShowDialogWithViewModel(vm => vm.PartText = textPart.Text); + tempFileName = GeneralExtensions.GetOriginalFileName(Path.GetTempPath(), mimePart.FileName); } - else if (part is MessagePart messagePart) + else { - this._viewModelWindowManager.ShowDialogWithViewModel(vm => vm.PartText = messagePart.Message.ToString()); + tempFileName = Path.GetTempFileName(); + string extension = part.ContentType.GetExtension(); + + if (extension.IsSet()) + tempFileName = Path.ChangeExtension(tempFileName, extension); } - else if (part is MimePart mimePart) - { - string tempFileName; - if (mimePart.FileName.IsSet()) - { - tempFileName = GeneralExtensions.GetOriginalFileName(Path.GetTempPath(), mimePart.FileName); - } - else + try + { + using (FileStream outputFile = File.Open(tempFileName, FileMode.Create)) { - tempFileName = Path.GetTempFileName(); - string extension = part.ContentType.GetExtension(); - - if (extension.IsSet()) - tempFileName = Path.ChangeExtension(tempFileName, extension); + mimePart.Content.DecodeTo(outputFile); } - try - { - using (FileStream outputFile = File.Open(tempFileName, FileMode.Create)) - { - mimePart.Content.DecodeTo(outputFile); - } - - Process.Start(new ProcessStartInfo(tempFileName)); - } - catch (Exception ex) - { - this._logger.Error(ex, "Failure Creating and Opening Up Attachment File: {TempFileName}", tempFileName); - MessageBox.Show($"Failed to Open Attachment File: {ex.Message}", - "Unable to Open Attachment"); - } + Process.Start(new ProcessStartInfo(tempFileName)); + } + catch (Exception ex) + { + this._logger.Error(ex, "Failure Creating and Opening Up Attachment File: {TempFileName}", tempFileName); + MessageBox.Show($"Failed to Open Attachment File: {ex.Message}", + "Unable to Open Attachment"); } } + } - public void SaveAs() + public void SaveAs() + { + if (this.SelectedPart is MimePart mimePart) { - if (this.SelectedPart is MimePart mimePart) + var dlg = new SaveFileDialog(); + if (!string.IsNullOrWhiteSpace(mimePart.FileName)) + dlg.FileName = mimePart.FileName; + + var extensions = new List(); + if (mimePart.ContentType.MediaSubtype != "Unknown") { - var dlg = new SaveFileDialog(); - if (!string.IsNullOrWhiteSpace(mimePart.FileName)) - dlg.FileName = mimePart.FileName; + string extension = mimePart.ContentType.GetExtension(); - var extensions = new List(); - if (mimePart.ContentType.MediaSubtype != "Unknown") + if (!string.IsNullOrWhiteSpace(extension)) { - string extension = mimePart.ContentType.GetExtension(); - - if (!string.IsNullOrWhiteSpace(extension)) - { - extensions.Add(string.Format("{0} (*{1})|*{1}", - Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(mimePart.ContentType.MediaSubtype), - extension)); - } + extensions.Add(string.Format("{0} (*{1})|*{1}", + Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(mimePart.ContentType.MediaSubtype), + extension)); } + } - extensions.Add("All (*.*)|*.*"); - dlg.Filter = string.Join("|", extensions.ToArray()); + extensions.Add("All (*.*)|*.*"); + dlg.Filter = string.Join("|", extensions.ToArray()); - bool? result = dlg.ShowDialog(); + bool? result = dlg.ShowDialog(); - if (result.HasValue && result.Value) - { - this._logger.Debug("Saving File {File} as Output for MimePart {PartFileName}", dlg.FileName, - mimePart.FileName); + if (result.HasValue && result.Value) + { + this._logger.Debug("Saving File {File} as Output for MimePart {PartFileName}", dlg.FileName, + mimePart.FileName); - // save it… - using Stream outputFile = dlg.OpenFile(); + // save it… + using Stream outputFile = dlg.OpenFile(); - mimePart.Content.DecodeTo(outputFile); - } + mimePart.Content.DecodeTo(outputFile); } - else if (this.SelectedPart is MessagePart messagePart) - { - var dlg = new SaveFileDialog(); + } + else if (this.SelectedPart is MessagePart messagePart) + { + var dlg = new SaveFileDialog(); - var fileName = this._messageRepository.GetFullMailFilename(messagePart.Message.Subject); + var fileName = this._messageRepository.GetFullMailFilename(messagePart.Message.Subject); - dlg.FileName = Path.GetFileName(fileName); - dlg.InitialDirectory = Path.GetDirectoryName(fileName); + dlg.FileName = Path.GetFileName(fileName); + dlg.InitialDirectory = Path.GetDirectoryName(fileName); - var extensions = new List {"Email (*.eml)|*.eml", "All (*.*)|*.*"}; + var extensions = new List {"Email (*.eml)|*.eml", "All (*.*)|*.*"}; - dlg.Filter = string.Join("|", extensions); + dlg.Filter = string.Join("|", extensions); - bool? result = dlg.ShowDialog(); + bool? result = dlg.ShowDialog(); - if (result.HasValue && result.Value) - { - this._logger.Debug("Saved Embedded Message as {MessageFile}", dlg.FileName); + if (result.HasValue && result.Value) + { + this._logger.Debug("Saved Embedded Message as {MessageFile}", dlg.FileName); - // save it… - using Stream outputFile = dlg.OpenFile(); + // save it… + using Stream outputFile = dlg.OpenFile(); - messagePart.Message.WriteTo(outputFile); - } + messagePart.Message.WriteTo(outputFile); } } + } - void RefreshParts() - { - this.Parts.Clear(); - this.Parts.AddRange(this.MimeMessage.BodyParts); - } + void RefreshParts() + { + this.Parts.Clear(); + this.Parts.AddRange(this.MimeMessage.BodyParts); } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/MessageDetailRawViewModel.cs b/src/Papercut.UI/ViewModels/MessageDetailRawViewModel.cs index 22e99c09..977204ad 100644 --- a/src/Papercut.UI/ViewModels/MessageDetailRawViewModel.cs +++ b/src/Papercut.UI/ViewModels/MessageDetailRawViewModel.cs @@ -30,119 +30,118 @@ using Papercut.Message.Helpers; using Papercut.Views; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public class MessageDetailRawViewModel : Screen, IMessageDetailItem { - public class MessageDetailRawViewModel : Screen, IMessageDetailItem - { - readonly ILogger _logger; + readonly ILogger _logger; - bool _isLoading; + bool _isLoading; - bool _messageLoaded; + bool _messageLoaded; - IDisposable? _messageLoader; + IDisposable? _messageLoader; - MimeMessage? _mimeMessage; + MimeMessage? _mimeMessage; - string? _raw; + string? _raw; - public MessageDetailRawViewModel(ILogger logger) - { - this.DisplayName = "Raw"; - this._logger = logger; - } + public MessageDetailRawViewModel(ILogger logger) + { + this.DisplayName = "Raw"; + this._logger = logger; + } - public string? Raw + public string? Raw + { + get => this._raw; + set { - get => this._raw; - set - { - this._raw = value; - this.NotifyOfPropertyChange(() => this.Raw); - } + this._raw = value; + this.NotifyOfPropertyChange(() => this.Raw); } + } - public MimeMessage? MimeMessage + public MimeMessage? MimeMessage + { + get => this._mimeMessage; + set { - get => this._mimeMessage; - set - { - this._mimeMessage = value; - this.NotifyOfPropertyChange(() => this.MimeMessage); - this.MessageLoaded = false; - } + this._mimeMessage = value; + this.NotifyOfPropertyChange(() => this.MimeMessage); + this.MessageLoaded = false; } + } - public bool MessageLoaded + public bool MessageLoaded + { + get => this._messageLoaded; + set { - get => this._messageLoaded; - set + this._messageLoaded = value; + if (!this._messageLoaded) { - this._messageLoaded = value; - if (!this._messageLoaded) - { - this.Raw = null; - } + this.Raw = null; } } + } - public bool IsLoading + public bool IsLoading + { + get => this._isLoading; + set { - get => this._isLoading; - set - { - this._isLoading = value; - this.NotifyOfPropertyChange(() => this.IsLoading); - } + this._isLoading = value; + this.NotifyOfPropertyChange(() => this.IsLoading); } + } - void RefreshDump() - { - if (this.MessageLoaded) - return; - - this.IsLoading = true; - - if (this._messageLoader != null) - { - this._messageLoader.Dispose(); - this._messageLoader = null; - } + void RefreshDump() + { + if (this.MessageLoaded) + return; - this._messageLoader = - Observable.Start(() => this._mimeMessage.GetStringDump()) - .SubscribeOn(TaskPoolScheduler.Default) - .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe(h => - { - this.Raw = h; - this.MessageLoaded = true; - }); - } + this.IsLoading = true; - protected override void OnViewLoaded(object view) + if (this._messageLoader != null) { - base.OnViewLoaded(view); - - if (view is not MessageDetailRawView typedView) - { - this._logger.Error("Unable to locate the MessageDetailRawView to hook the Text Control"); - return; - } + this._messageLoader.Dispose(); + this._messageLoader = null; + } - this.GetPropertyValues(p => p.Raw) + this._messageLoader = + Observable.Start(() => this._mimeMessage.GetStringDump()) + .SubscribeOn(TaskPoolScheduler.Default) .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe(s => + .Subscribe(h => { - typedView.rawEdit.Document = new TextDocument(new StringTextSource(s ?? string.Empty)); - this.IsLoading = false; + this.Raw = h; + this.MessageLoaded = true; }); - } + } + + protected override void OnViewLoaded(object view) + { + base.OnViewLoaded(view); - protected override Task OnActivateAsync(CancellationToken token) + if (view is not MessageDetailRawView typedView) { - this.RefreshDump(); - return base.OnActivateAsync(token); + this._logger.Error("Unable to locate the MessageDetailRawView to hook the Text Control"); + return; } + + this.GetPropertyValues(p => p.Raw) + .ObserveOn(Dispatcher.CurrentDispatcher) + .Subscribe(s => + { + typedView.rawEdit.Document = new TextDocument(new StringTextSource(s ?? string.Empty)); + this.IsLoading = false; + }); + } + + protected override Task OnActivateAsync(CancellationToken token) + { + this.RefreshDump(); + return base.OnActivateAsync(token); } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/MessageDetailViewModel.cs b/src/Papercut.UI/ViewModels/MessageDetailViewModel.cs index 4accd837..90a4b265 100644 --- a/src/Papercut.UI/ViewModels/MessageDetailViewModel.cs +++ b/src/Papercut.UI/ViewModels/MessageDetailViewModel.cs @@ -28,320 +28,319 @@ using Papercut.Message; using Papercut.Message.Helpers; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public class MessageDetailViewModel : Conductor.Collection.OneActive { - public class MessageDetailViewModel : Conductor.Collection.OneActive - { - readonly MimeMessageLoader _mimeMessageLoader; + readonly MimeMessageLoader _mimeMessageLoader; - int _attachmentCount; + int _attachmentCount; - string? _bcc; + string? _bcc; - string? _cc; + string? _cc; - string? _date; + string? _date; - string? _from; + string? _from; - string? _htmlFile; + string? _htmlFile; - bool _isHtml; + bool _isHtml; - bool _isLoading; + bool _isLoading; - IDisposable? _loadingDisposable; + IDisposable? _loadingDisposable; - private string? _priority; + private string? _priority; - private string? _priorityColor; + private string? _priorityColor; - int _selectedTabIndex; + int _selectedTabIndex; - string? _subject; + string? _subject; - string? _textBody; + string? _textBody; - string? _to; + string? _to; - public MessageDetailViewModel( - Func partsListViewModelFactory, - Func htmlViewModelFactory, - Func rawViewModelFactory, - Func headerViewModelFactory, - Func bodyViewModelFactory, - MimeMessageLoader mimeMessageLoader) - { - this._mimeMessageLoader = mimeMessageLoader; - - this.PartsListViewModel = partsListViewModelFactory(); - this.HtmlViewModel = htmlViewModelFactory(); - this.RawViewModel = rawViewModelFactory(); - this.HeaderViewModel = headerViewModelFactory(); - this.BodyViewModel = bodyViewModelFactory(); - - this.Items.Add(this.HtmlViewModel); - this.Items.Add(this.HeaderViewModel); - this.Items.Add(this.BodyViewModel); - this.Items.Add(this.PartsListViewModel); - this.Items.Add(this.RawViewModel); - } + public MessageDetailViewModel( + Func partsListViewModelFactory, + Func htmlViewModelFactory, + Func rawViewModelFactory, + Func headerViewModelFactory, + Func bodyViewModelFactory, + MimeMessageLoader mimeMessageLoader) + { + this._mimeMessageLoader = mimeMessageLoader; + + this.PartsListViewModel = partsListViewModelFactory(); + this.HtmlViewModel = htmlViewModelFactory(); + this.RawViewModel = rawViewModelFactory(); + this.HeaderViewModel = headerViewModelFactory(); + this.BodyViewModel = bodyViewModelFactory(); + + this.Items.Add(this.HtmlViewModel); + this.Items.Add(this.HeaderViewModel); + this.Items.Add(this.BodyViewModel); + this.Items.Add(this.PartsListViewModel); + this.Items.Add(this.RawViewModel); + } - public string? Subject + public string? Subject + { + get => this._subject; + set { - get => this._subject; - set - { - this._subject = value; - this.NotifyOfPropertyChange(() => this.Subject); - } + this._subject = value; + this.NotifyOfPropertyChange(() => this.Subject); } + } - public string? To + public string? To + { + get => this._to; + set { - get => this._to; - set - { - this._to = value; - this.NotifyOfPropertyChange(() => this.To); - } + this._to = value; + this.NotifyOfPropertyChange(() => this.To); } + } - public string? Bcc + public string? Bcc + { + get => this._bcc; + set { - get => this._bcc; - set - { - this._bcc = value; - this.NotifyOfPropertyChange(() => this.Bcc); - } + this._bcc = value; + this.NotifyOfPropertyChange(() => this.Bcc); } + } - public string? Priority + public string? Priority + { + get => this._priority; + set { - get => this._priority; - set - { - this._priority = value; - this.NotifyOfPropertyChange(() => this.Priority); - } + this._priority = value; + this.NotifyOfPropertyChange(() => this.Priority); } + } - public string? PriorityColor + public string? PriorityColor + { + get => this._priorityColor; + set { - get => this._priorityColor; - set - { - this._priorityColor = value; - this.NotifyOfPropertyChange(() => this.PriorityColor); - } + this._priorityColor = value; + this.NotifyOfPropertyChange(() => this.PriorityColor); } + } - public string? Date + public string? Date + { + get => this._date; + set { - get => this._date; - set - { - this._date = value; - this.NotifyOfPropertyChange(() => this.Date); - } + this._date = value; + this.NotifyOfPropertyChange(() => this.Date); } + } - public string? From + public string? From + { + get => this._from; + set { - get => this._from; - set - { - this._from = value; - this.NotifyOfPropertyChange(() => this.From); - } + this._from = value; + this.NotifyOfPropertyChange(() => this.From); } + } - public string? CC + public string? CC + { + get => this._cc; + set { - get => this._cc; - set - { - this._cc = value; - this.NotifyOfPropertyChange(() => this.CC); - } + this._cc = value; + this.NotifyOfPropertyChange(() => this.CC); } + } - public string? TextBody + public string? TextBody + { + get => this._textBody; + set { - get => this._textBody; - set - { - this._textBody = value; - this.NotifyOfPropertyChange(() => this.TextBody); - } + this._textBody = value; + this.NotifyOfPropertyChange(() => this.TextBody); } + } - public bool IsLoading + public bool IsLoading + { + get => this._isLoading; + set { - get => this._isLoading; - set - { - this._isLoading = value; - this.NotifyOfPropertyChange(() => this.IsLoading); - } + this._isLoading = value; + this.NotifyOfPropertyChange(() => this.IsLoading); } + } - public bool IsHtml + public bool IsHtml + { + get => this._isHtml; + set { - get => this._isHtml; - set - { - this._isHtml = value; - this.NotifyOfPropertyChange(() => this.IsHtml); - } + this._isHtml = value; + this.NotifyOfPropertyChange(() => this.IsHtml); } + } - public int SelectedTabIndex + public int SelectedTabIndex + { + get => this._selectedTabIndex; + set { - get => this._selectedTabIndex; - set - { - this._selectedTabIndex = value; - this.NotifyOfPropertyChange(() => this.SelectedTabIndex); - } + this._selectedTabIndex = value; + this.NotifyOfPropertyChange(() => this.SelectedTabIndex); } + } - public int AttachmentCount + public int AttachmentCount + { + get => this._attachmentCount; + set { - get => this._attachmentCount; - set - { - this._attachmentCount = value; - this.NotifyOfPropertyChange(() => this.AttachmentCount); - this.NotifyOfPropertyChange(() => this.HasAttachments); - } + this._attachmentCount = value; + this.NotifyOfPropertyChange(() => this.AttachmentCount); + this.NotifyOfPropertyChange(() => this.HasAttachments); } + } - public bool HasAttachments => this.AttachmentCount > 0; + public bool HasAttachments => this.AttachmentCount > 0; - public string? HtmlFile + public string? HtmlFile + { + get => this._htmlFile; + set { - get => this._htmlFile; - set - { - this._htmlFile = value; - this.NotifyOfPropertyChange(() => this.HtmlFile); - } + this._htmlFile = value; + this.NotifyOfPropertyChange(() => this.HtmlFile); } + } - public MessageDetailPartsListViewModel PartsListViewModel { get; } + public MessageDetailPartsListViewModel PartsListViewModel { get; } - public MessageDetailHtmlViewModel HtmlViewModel { get; } + public MessageDetailHtmlViewModel HtmlViewModel { get; } - public MessageDetailRawViewModel RawViewModel { get; } + public MessageDetailRawViewModel RawViewModel { get; } - public MessageDetailHeaderViewModel HeaderViewModel { get; } + public MessageDetailHeaderViewModel HeaderViewModel { get; } - public MessageDetailBodyViewModel BodyViewModel { get; } + public MessageDetailBodyViewModel BodyViewModel { get; } - public void LoadMessageEntry(MessageEntry? messageEntry) - { - this._loadingDisposable?.Dispose(); + public void LoadMessageEntry(MessageEntry? messageEntry) + { + this._loadingDisposable?.Dispose(); - var handleLoading = !this.IsLoading; + var handleLoading = !this.IsLoading; - if (messageEntry == null) - { - // show empty... - this.DisplayMimeMessage(null); - if (handleLoading) this.IsLoading = false; - } - else - { - if (handleLoading) this.IsLoading = true; + if (messageEntry == null) + { + // show empty... + this.DisplayMimeMessage(null); + if (handleLoading) this.IsLoading = false; + } + else + { + if (handleLoading) this.IsLoading = true; - // load and show it... - this._loadingDisposable = this._mimeMessageLoader.GetObservable(messageEntry).ObserveOn(Dispatcher.CurrentDispatcher).Subscribe(m => + // load and show it... + this._loadingDisposable = this._mimeMessageLoader.GetObservable(messageEntry).ObserveOn(Dispatcher.CurrentDispatcher).Subscribe(m => { this.DisplayMimeMessage(m); if (handleLoading) this.IsLoading = false; }, - e => - { - var failureMessage = - MimeMessage.CreateFromMailMessage(MailMessageHelper.CreateFailureMailMessage(e.Message)); - - this.DisplayMimeMessage(failureMessage); - if (handleLoading) this.IsLoading = false; - }); - } + e => + { + var failureMessage = + MimeMessage.CreateFromMailMessage(MailMessageHelper.CreateFailureMailMessage(e.Message)); + + this.DisplayMimeMessage(failureMessage); + if (handleLoading) this.IsLoading = false; + }); } + } - void DisplayMimeMessage(MimeMessage? mailMessageEx) + void DisplayMimeMessage(MimeMessage? mailMessageEx) + { + (string? Name, string Color) GetPriorityText(MimeMessage? message) { - (string? Name, string Color) GetPriorityText(MimeMessage? message) - { - ArgumentNullException.ThrowIfNull(message); - - switch (message.Priority) - { - case MessagePriority.NonUrgent: return ("Low", "Blue"); - case MessagePriority.Urgent: return ("High", "Red"); - case MessagePriority.Normal: - break; - } + ArgumentNullException.ThrowIfNull(message); - return default; + switch (message.Priority) + { + case MessagePriority.NonUrgent: return ("Low", "Blue"); + case MessagePriority.Urgent: return ("High", "Red"); + case MessagePriority.Normal: + break; } - this.ResetMessage(); + return default; + } - if (mailMessageEx != null) - { - this.HeaderViewModel.Headers = string.Join("\r\n", mailMessageEx.Headers.Select(h => h.ToString())); + this.ResetMessage(); + + if (mailMessageEx != null) + { + this.HeaderViewModel.Headers = string.Join("\r\n", mailMessageEx.Headers.Select(h => h.ToString())); - var parts = mailMessageEx.BodyParts.OfType().ToList(); - var mainBody = parts.GetMainBodyTextPart(); + var parts = mailMessageEx.BodyParts.OfType().ToList(); + var mainBody = parts.GetMainBodyTextPart(); - this.From = mailMessageEx.From?.ToString() ?? string.Empty; - this.To = mailMessageEx.To?.ToString() ?? string.Empty; - this.CC = mailMessageEx.Cc?.ToString() ?? string.Empty; - this.Bcc = mailMessageEx.Bcc?.ToString() ?? string.Empty; - var priority = GetPriorityText(mailMessageEx); - this.Priority = priority.Name; - this.PriorityColor = priority.Color ?? "Black"; - this.Date = mailMessageEx.Date.ToString(); - this.Subject = mailMessageEx.Subject ?? string.Empty; + this.From = mailMessageEx.From?.ToString() ?? string.Empty; + this.To = mailMessageEx.To?.ToString() ?? string.Empty; + this.CC = mailMessageEx.Cc?.ToString() ?? string.Empty; + this.Bcc = mailMessageEx.Bcc?.ToString() ?? string.Empty; + var priority = GetPriorityText(mailMessageEx); + this.Priority = priority.Name; + this.PriorityColor = priority.Color ?? "Black"; + this.Date = mailMessageEx.Date.ToString(); + this.Subject = mailMessageEx.Subject ?? string.Empty; - this.AttachmentCount = parts.GetAttachments().Count(); + this.AttachmentCount = parts.GetAttachments().Count(); - this.RawViewModel.MimeMessage = mailMessageEx; - this.PartsListViewModel.MimeMessage = mailMessageEx; + this.RawViewModel.MimeMessage = mailMessageEx; + this.PartsListViewModel.MimeMessage = mailMessageEx; - this.BodyViewModel.Body = mainBody != null ? mainBody.GetText(Encoding.UTF8) : string.Empty; + this.BodyViewModel.Body = mainBody != null ? mainBody.GetText(Encoding.UTF8) : string.Empty; - if (mainBody != null) { - this.IsHtml = mainBody.IsContentHtml(); - this.HtmlViewModel.ShowMessage(mailMessageEx); + if (mainBody != null) { + this.IsHtml = mainBody.IsContentHtml(); + this.HtmlViewModel.ShowMessage(mailMessageEx); - if (this.IsHtml) - { - var textPartNotHtml = parts.OfType().Except(new[] { mainBody }).FirstOrDefault(); - if (textPartNotHtml != null) this.TextBody = textPartNotHtml.GetText(Encoding.UTF8); - } + if (this.IsHtml) + { + var textPartNotHtml = parts.OfType().Except(new[] { mainBody }).FirstOrDefault(); + if (textPartNotHtml != null) this.TextBody = textPartNotHtml.GetText(Encoding.UTF8); } } - - this.SelectedTabIndex = 0; } - void ResetMessage() - { - this.AttachmentCount = 0; - this.IsHtml = false; - this.HtmlFile = null; - this.TextBody = null; - - this.HtmlViewModel.HtmlFile = null; - this.HeaderViewModel.Headers = null; - this.BodyViewModel.Body = null; - this.PartsListViewModel.MimeMessage = null; - } + this.SelectedTabIndex = 0; + } + + void ResetMessage() + { + this.AttachmentCount = 0; + this.IsHtml = false; + this.HtmlFile = null; + this.TextBody = null; + + this.HtmlViewModel.HtmlFile = null; + this.HeaderViewModel.Headers = null; + this.BodyViewModel.Body = null; + this.PartsListViewModel.MimeMessage = null; } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/MessageListViewModel.cs b/src/Papercut.UI/ViewModels/MessageListViewModel.cs index 21f65519..abefbf81 100644 --- a/src/Papercut.UI/ViewModels/MessageListViewModel.cs +++ b/src/Papercut.UI/ViewModels/MessageListViewModel.cs @@ -47,415 +47,414 @@ using ListBox = System.Windows.Controls.ListBox; -namespace Papercut.ViewModels -{ - using Action = System.Action; - using KeyEventArgs = System.Windows.Input.KeyEventArgs; - using Screen = Caliburn.Micro.Screen; - - public class MessageListViewModel : Screen, IHandle - { - readonly ILogger _logger; +namespace Papercut.ViewModels; - readonly MessageRepository _messageRepository; +using Action = System.Action; +using KeyEventArgs = System.Windows.Input.KeyEventArgs; +using Screen = Caliburn.Micro.Screen; - readonly MessageWatcher _messageWatcher; +public class MessageListViewModel : Screen, IHandle +{ + readonly ILogger _logger; - readonly MimeMessageLoader _mimeMessageLoader; + readonly MessageRepository _messageRepository; - private readonly ExplorerProcessService _explorerProcessService; + readonly MessageWatcher _messageWatcher; - private readonly IUiCommandHub _uiCommandHub; + readonly MimeMessageLoader _mimeMessageLoader; - bool _isLoading; + private readonly ExplorerProcessService _explorerProcessService; - private int? _previousIndex; + private readonly IUiCommandHub _uiCommandHub; - private ListBox _messageListBox; + bool _isLoading; - public MessageListViewModel( - IUiCommandHub uiCommandHub, - MessageRepository messageRepository, - MessageWatcher messageWatcher, - MimeMessageLoader mimeMessageLoader, - ExplorerProcessService explorerProcessService, - ILogger logger) - { - ArgumentNullException.ThrowIfNull(messageRepository); - ArgumentNullException.ThrowIfNull(messageWatcher); - ArgumentNullException.ThrowIfNull(mimeMessageLoader); - - this._uiCommandHub = uiCommandHub; - this._messageRepository = messageRepository; - this._messageWatcher = messageWatcher; - this._mimeMessageLoader = mimeMessageLoader; - this._explorerProcessService = explorerProcessService; - this._logger = logger; - - this.SetupMessages(); - this.RefreshMessageList(); - } + private int? _previousIndex; - public ObservableCollection Messages { get; private set; } + private ListBox _messageListBox; - public ICollectionView MessagesSorted { get; private set; } + public MessageListViewModel( + IUiCommandHub uiCommandHub, + MessageRepository messageRepository, + MessageWatcher messageWatcher, + MimeMessageLoader mimeMessageLoader, + ExplorerProcessService explorerProcessService, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(messageRepository); + ArgumentNullException.ThrowIfNull(messageWatcher); + ArgumentNullException.ThrowIfNull(mimeMessageLoader); + + this._uiCommandHub = uiCommandHub; + this._messageRepository = messageRepository; + this._messageWatcher = messageWatcher; + this._mimeMessageLoader = mimeMessageLoader; + this._explorerProcessService = explorerProcessService; + this._logger = logger; + + this.SetupMessages(); + this.RefreshMessageList(); + } - public MimeMessageEntry? SelectedMessage => this.GetSelected().FirstOrDefault(); + public ObservableCollection Messages { get; private set; } - public string DeleteText => UIStrings.DeleteTextTemplate.RenderTemplate(this); + public ICollectionView MessagesSorted { get; private set; } - public bool HasSelectedMessage => this.GetSelected().Any(); + public MimeMessageEntry? SelectedMessage => this.GetSelected().FirstOrDefault(); - public bool HasMessages => this.Messages.Any(); + public string DeleteText => UIStrings.DeleteTextTemplate.RenderTemplate(this); - public int SelectedMessageCount => this.GetSelected().Count(); + public bool HasSelectedMessage => this.GetSelected().Any(); - public string SelectedMessageCountHuman - { - get - { - var count = this.SelectedMessageCount; + public bool HasMessages => this.Messages.Any(); - if (count < 1000) return count.ToString(); - if (count < 1000000) - { - return $"{(double)count / 1000:##.#}K"; - } - - // do I need to support millions? probably not but why not... - return $"{(double)count / 1000000:##.##}M"; - } - } + public int SelectedMessageCount => this.GetSelected().Count(); - public bool IsLoading + public string SelectedMessageCountHuman + { + get { - get => this._isLoading; - set + var count = this.SelectedMessageCount; + + if (count < 1000) return count.ToString(); + if (count < 1000000) { - this._isLoading = value; - this.NotifyOfPropertyChange(() => this.IsLoading); + return $"{(double)count / 1000:##.#}K"; } + + // do I need to support millions? probably not but why not... + return $"{(double)count / 1000000:##.##}M"; } + } - private ListSortDirection SortOrder => Enum.TryParse(Settings.Default.MessageListSortOrder, out var sortOrder) - ? sortOrder - : ListSortDirection.Ascending; - - public Task HandleAsync(SettingsUpdatedEvent message, CancellationToken token) + public bool IsLoading + { + get => this._isLoading; + set { - using (this.MessagesSorted.DeferRefresh()) - { - this.MessagesSorted.SortDescriptions.Clear(); - this.MessagesSorted.SortDescriptions.Add(new SortDescription(nameof(MessageEntry.SortTicks), this.SortOrder)); - } - - return Task.CompletedTask; + this._isLoading = value; + this.NotifyOfPropertyChange(() => this.IsLoading); } + } - MimeMessageEntry? GetMessageByIndex(int index) + private ListSortDirection SortOrder => Enum.TryParse(Settings.Default.MessageListSortOrder, out var sortOrder) + ? sortOrder + : ListSortDirection.Ascending; + + public Task HandleAsync(SettingsUpdatedEvent message, CancellationToken token) + { + using (this.MessagesSorted.DeferRefresh()) { - return this.MessagesSorted.OfType().Skip(index).FirstOrDefault(); + this.MessagesSorted.SortDescriptions.Clear(); + this.MessagesSorted.SortDescriptions.Add(new SortDescription(nameof(MessageEntry.SortTicks), this.SortOrder)); } - int? GetIndexOfMessage(MessageEntry? entry) - { - if (entry == null) - return null; + return Task.CompletedTask; + } - int index = this.MessagesSorted.OfType().FindIndex(m => Equals(entry, m)); + MimeMessageEntry? GetMessageByIndex(int index) + { + return this.MessagesSorted.OfType().Skip(index).FirstOrDefault(); + } - return index == -1 ? null : index; - } + int? GetIndexOfMessage(MessageEntry? entry) + { + if (entry == null) + return null; - void PushSelectedIndex() - { - if (this._previousIndex.HasValue) - { - return; - } + int index = this.MessagesSorted.OfType().FindIndex(m => Equals(entry, m)); - var selectedMessage = this.SelectedMessage; + return index == -1 ? null : index; + } - if (selectedMessage != null) - { - this._previousIndex = this.GetIndexOfMessage(selectedMessage); - } + void PushSelectedIndex() + { + if (this._previousIndex.HasValue) + { + return; } - void PopSelectedIndex() + var selectedMessage = this.SelectedMessage; + + if (selectedMessage != null) { - this._previousIndex = null; + this._previousIndex = this.GetIndexOfMessage(selectedMessage); } + } - void SetupMessages() - { - this.Messages = new ObservableCollection(); - this.MessagesSorted = CollectionViewSource.GetDefaultView(this.Messages); - this.MessagesSorted.SortDescriptions.Add(new SortDescription(nameof(MessageEntry.SortTicks), this.SortOrder)); + void PopSelectedIndex() + { + this._previousIndex = null; + } + + void SetupMessages() + { + this.Messages = new ObservableCollection(); + this.MessagesSorted = CollectionViewSource.GetDefaultView(this.Messages); + this.MessagesSorted.SortDescriptions.Add(new SortDescription(nameof(MessageEntry.SortTicks), this.SortOrder)); - // Begin listening for new messages - this._messageWatcher.NewMessage += this.NewMessage; + // Begin listening for new messages + this._messageWatcher.NewMessage += this.NewMessage; - Observable.FromEventPattern( + Observable.FromEventPattern( e => this._messageWatcher.RefreshNeeded += e, e => this._messageWatcher.RefreshNeeded -= e, TaskPoolScheduler.Default) - .Throttle(TimeSpan.FromMilliseconds(100)) - .ObserveOn(Dispatcher.CurrentDispatcher) - .Subscribe(_ => this.RefreshMessageList()); + .Throttle(TimeSpan.FromMilliseconds(100)) + .ObserveOn(Dispatcher.CurrentDispatcher) + .Subscribe(_ => this.RefreshMessageList()); - this.Messages.CollectionChanged += this.CollectionChanged; - } + this.Messages.CollectionChanged += this.CollectionChanged; + } - void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + try { - try - { - var notifyOfSelectionChange = new Action( - () => - { - this.NotifyOfPropertyChange(() => this.HasSelectedMessage); - this.NotifyOfPropertyChange(() => this.SelectedMessageCount); - this.NotifyOfPropertyChange(() => this.SelectedMessage); - this.NotifyOfPropertyChange(() => this.DeleteText); - }); - - if (args.NewItems != null) + var notifyOfSelectionChange = new Action( + () => { - foreach (MimeMessageEntry m in args.NewItems.OfType()) - { - m.PropertyChanged += (_, _) => notifyOfSelectionChange(); - } - } + this.NotifyOfPropertyChange(() => this.HasSelectedMessage); + this.NotifyOfPropertyChange(() => this.SelectedMessageCount); + this.NotifyOfPropertyChange(() => this.SelectedMessage); + this.NotifyOfPropertyChange(() => this.DeleteText); + }); - notifyOfSelectionChange(); - } - catch (Exception ex) + if (args.NewItems != null) { - this._logger.Error(ex, "Failure Handling Message Collection Change {@Args}", args); + foreach (MimeMessageEntry m in args.NewItems.OfType()) + { + m.PropertyChanged += (_, _) => notifyOfSelectionChange(); + } } - } - void AddNewMessage(MessageEntry entry) + notifyOfSelectionChange(); + } + catch (Exception ex) { - var observable = this._mimeMessageLoader.GetObservable(entry); - - observable.ObserveOn(Dispatcher.CurrentDispatcher).Subscribe( - message => - { - this._uiCommandHub.ShowBalloonTip( - 3500, - "New Message Received", - $"From: {message.From.ToString().Truncate(50, "...")}\r\nSubject: {message.Subject.Truncate(50)}", - ToolTipIcon.Info); - - this.Messages.Add(new MimeMessageEntry(entry, this._mimeMessageLoader)); - - // handle selection if nothing is selected - this.ValidateSelected(); - }, - _ => - { - // NOOP - }); + this._logger.Error(ex, "Failure Handling Message Collection Change {@Args}", args); } + } - public int? TryGetValidSelectedIndex(int? previousIndex = null) - { - int messageCount = this.Messages.Count; + void AddNewMessage(MessageEntry entry) + { + var observable = this._mimeMessageLoader.GetObservable(entry); - if (messageCount == 0) + observable.ObserveOn(Dispatcher.CurrentDispatcher).Subscribe( + message => { - return null; - } + this._uiCommandHub.ShowBalloonTip( + 3500, + "New Message Received", + $"From: {message.From.ToString().Truncate(50, "...")}\r\nSubject: {message.Subject.Truncate(50)}", + ToolTipIcon.Info); + + this.Messages.Add(new MimeMessageEntry(entry, this._mimeMessageLoader)); + + // handle selection if nothing is selected + this.ValidateSelected(); + }, + _ => + { + // NOOP + }); + } - int? index = null; + public int? TryGetValidSelectedIndex(int? previousIndex = null) + { + int messageCount = this.Messages.Count; - if (previousIndex.HasValue) - { - index = previousIndex; + if (messageCount == 0) + { + return null; + } - if (index >= messageCount) - { - index = messageCount - 1; - } - } + int? index = null; - if (index <= 0 || index >= messageCount) - { - index = null; - } + if (previousIndex.HasValue) + { + index = previousIndex; - // select the bottom - if (!index.HasValue) + if (index >= messageCount) { - if (this.SortOrder == ListSortDirection.Ascending) - { - index = messageCount - 1; - } - else - { - index = 0; - } + index = messageCount - 1; } - - return index; } - private void SelectMessageByIndex(int index) + if (index <= 0 || index >= messageCount) { - this.TrySelectMessage(this.GetMessageByIndex(index)); + index = null; } - public void OpenMessageFolder() + // select the bottom + if (!index.HasValue) { - var folders = this.GetSelected().IfNullEmpty().Select(s => Path.GetDirectoryName(s.File)) - .WhereNotNull() - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var folder in folders) + if (this.SortOrder == ListSortDirection.Ascending) { - this._explorerProcessService.OpenFolder(folder); + index = messageCount - 1; } - } - - public void ValidateSelected() - { - if (this.SelectedMessageCount != 0 || this.Messages.Count == 0) return; - - var index = this.TryGetValidSelectedIndex(this._previousIndex); - if (index.HasValue) + else { - this.SelectMessageByIndex(index.Value); + index = 0; } } - void NewMessage(object sender, NewMessageEventArgs e) - { - Execute.OnUIThread(() => this.AddNewMessage(e.NewMessage)); - } + return index; + } - public IEnumerable GetSelected() + private void SelectMessageByIndex(int index) + { + this.TrySelectMessage(this.GetMessageByIndex(index)); + } + + public void OpenMessageFolder() + { + var folders = this.GetSelected().IfNullEmpty().Select(s => Path.GetDirectoryName(s.File)) + .WhereNotNull() + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var folder in folders) { - return this.Messages.Where(message => message.IsSelected); + this._explorerProcessService.OpenFolder(folder); } + } - public void ClearSelected() + public void ValidateSelected() + { + if (this.SelectedMessageCount != 0 || this.Messages.Count == 0) return; + + var index = this.TryGetValidSelectedIndex(this._previousIndex); + if (index.HasValue) { - foreach (MimeMessageEntry message in this.GetSelected().ToList()) - { - message.IsSelected = false; - } + this.SelectMessageByIndex(index.Value); } + } - public void DeleteAll() + void NewMessage(object sender, NewMessageEventArgs e) + { + Execute.OnUIThread(() => this.AddNewMessage(e.NewMessage)); + } + + public IEnumerable GetSelected() + { + return this.Messages.Where(message => message.IsSelected); + } + + public void ClearSelected() + { + foreach (MimeMessageEntry message in this.GetSelected().ToList()) { - this.ClearSelected(); - this.DeleteMessages(this.Messages.ToList()); + message.IsSelected = false; } + } - public void DeleteSelected() - { - // Lock to prevent rapid clicking issues - this.PushSelectedIndex(); + public void DeleteAll() + { + this.ClearSelected(); + this.DeleteMessages(this.Messages.ToList()); + } - var selectedMessageEntries = this.GetSelected().ToList(); + public void DeleteSelected() + { + // Lock to prevent rapid clicking issues + this.PushSelectedIndex(); - this.DeleteMessages(selectedMessageEntries); - } + var selectedMessageEntries = this.GetSelected().ToList(); + + this.DeleteMessages(selectedMessageEntries); + } - private void TrySelectMessage(MimeMessageEntry? message) + private void TrySelectMessage(MimeMessageEntry? message) + { + if (message != null) { - if (message != null) - { - message.IsSelected = true; - this._messageListBox?.ScrollIntoView(message); - } + message.IsSelected = true; + this._messageListBox?.ScrollIntoView(message); } + } - private List DeleteMessages(List selectedMessageEntries) - { - List failedEntries = - selectedMessageEntries.Select( - entry => + private List DeleteMessages(List selectedMessageEntries) + { + List failedEntries = + selectedMessageEntries.Select( + entry => + { + try { - try - { - this._messageRepository.DeleteMessage(entry); - return null; - } - catch (Exception ex) - { - this._logger.Error( - ex, - "Failure Deleting Message {EmailMessageFile}", - entry.File); - - return ex.Message; - } - }).Where(f => f != null).ToList()!; - - if (failedEntries.Any()) - { - this._uiCommandHub.ShowMessage( - string.Join("\r\n", failedEntries), - $"Failed to Delete Message{(failedEntries.Count > 1 ? "s" : string.Empty)}"); - } + this._messageRepository.DeleteMessage(entry); + return null; + } + catch (Exception ex) + { + this._logger.Error( + ex, + "Failure Deleting Message {EmailMessageFile}", + entry.File); - return failedEntries; - } + return ex.Message; + } + }).Where(f => f != null).ToList()!; - public void MessageListKeyDown(KeyEventArgs e) + if (failedEntries.Any()) { - if (e.Key != Key.Delete) - return; - - this.DeleteSelected(); + this._uiCommandHub.ShowMessage( + string.Join("\r\n", failedEntries), + $"Failed to Delete Message{(failedEntries.Count > 1 ? "s" : string.Empty)}"); } - public void RefreshMessageList() - { - this.PushSelectedIndex(); + return failedEntries; + } - List messageEntries = this._messageRepository.LoadMessages() - .ToList(); + public void MessageListKeyDown(KeyEventArgs e) + { + if (e.Key != Key.Delete) + return; - List toAdd = - messageEntries.Except(this.Messages) - .OrderBy(s => s.FileSize) - .Select(m => new MimeMessageEntry(m, this._mimeMessageLoader)) - .ToList(); + this.DeleteSelected(); + } - var toDelete = this.Messages.Except(messageEntries) - .OfType().ToList(); + public void RefreshMessageList() + { + this.PushSelectedIndex(); - toDelete.ForEach(m => this.Messages.Remove(m)); - this.Messages.AddRange(toAdd); + List messageEntries = this._messageRepository.LoadMessages() + .ToList(); - this.MessagesSorted.Refresh(); - this.ValidateSelected(); - this.PopSelectedIndex(); - } + List toAdd = + messageEntries.Except(this.Messages) + .OrderBy(s => s.FileSize) + .Select(m => new MimeMessageEntry(m, this._mimeMessageLoader)) + .ToList(); - protected override void OnViewLoaded(object view) - { - base.OnViewLoaded(view); + var toDelete = this.Messages.Except(messageEntries) + .OfType().ToList(); - if (view is MessageListView typedView) - { - this._messageListBox = typedView.MessagesList; - - // maybe scroll to selected - var mimeMessageEntry = this.SelectedMessage; - if (mimeMessageEntry != null) - this._messageListBox.ScrollIntoView(mimeMessageEntry); - } - } + toDelete.ForEach(m => this.Messages.Remove(m)); + this.Messages.AddRange(toAdd); + + this.MessagesSorted.Refresh(); + this.ValidateSelected(); + this.PopSelectedIndex(); + } + + protected override void OnViewLoaded(object view) + { + base.OnViewLoaded(view); - public void SelectMostRecentMessage() + if (view is MessageListView typedView) { - this.ClearSelected(); - this.TrySelectMessage(this.Messages.MaxBy(s => s.SortTicks)); + this._messageListBox = typedView.MessagesList; + + // maybe scroll to selected + var mimeMessageEntry = this.SelectedMessage; + if (mimeMessageEntry != null) + this._messageListBox.ScrollIntoView(mimeMessageEntry); } } + + public void SelectMostRecentMessage() + { + this.ClearSelected(); + this.TrySelectMessage(this.Messages.MaxBy(s => s.SortTicks)); + } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/MimePartViewModel.cs b/src/Papercut.UI/ViewModels/MimePartViewModel.cs index 54f36000..e21bd211 100644 --- a/src/Papercut.UI/ViewModels/MimePartViewModel.cs +++ b/src/Papercut.UI/ViewModels/MimePartViewModel.cs @@ -18,37 +18,36 @@ using Caliburn.Micro; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public class MimePartViewModel : Screen { - public class MimePartViewModel : Screen - { - string _partText; + string _partText; - string _windowTitle = "Mime Part Viewer"; + string _windowTitle = "Mime Part Viewer"; - public string WindowTitle + public string WindowTitle + { + get => this._windowTitle; + set { - get => this._windowTitle; - set - { - this._windowTitle = value; - this.NotifyOfPropertyChange(() => this.WindowTitle); - } + this._windowTitle = value; + this.NotifyOfPropertyChange(() => this.WindowTitle); } + } - public string PartText + public string PartText + { + get => this._partText; + set { - get => this._partText; - set - { - this._partText = value; - this.NotifyOfPropertyChange(() => this.PartText); - } + this._partText = value; + this.NotifyOfPropertyChange(() => this.PartText); } + } - public async Task Close() - { - await this.TryCloseAsync(false); - } + public async Task Close() + { + await this.TryCloseAsync(false); } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/OptionsViewModel.cs b/src/Papercut.UI/ViewModels/OptionsViewModel.cs index 4fc2c4a9..00350085 100644 --- a/src/Papercut.UI/ViewModels/OptionsViewModel.cs +++ b/src/Papercut.UI/ViewModels/OptionsViewModel.cs @@ -31,192 +31,191 @@ using Papercut.Infrastructure.Themes; using Papercut.Properties; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public class OptionsViewModel : Screen { - public class OptionsViewModel : Screen - { - const string WindowTitleDefault = "Options"; + const string WindowTitleDefault = "Options"; - static readonly Lazy> _ipList = new Lazy>(GetIPs); + static readonly Lazy> _ipList = new Lazy>(GetIPs); - readonly IMessageBus _messageBus; + readonly IMessageBus _messageBus; - private readonly ThemeColorRepository _themeColorRepository; + private readonly ThemeColorRepository _themeColorRepository; - string _ip = "Any"; + string _ip = "Any"; - string _messageListSortOrder = "Descending"; + string _messageListSortOrder = "Descending"; - bool _minimizeOnClose; + bool _minimizeOnClose; - private bool _minimizeToTray; + private bool _minimizeToTray; - int _port = 25; + int _port = 25; - bool _runOnStartup; + bool _runOnStartup; - private bool _showNotifications; + private bool _showNotifications; - bool _startMinimized; + bool _startMinimized; - private ThemeColor _themeColor; + private ThemeColor _themeColor; - string _windowTitle = WindowTitleDefault; + string _windowTitle = WindowTitleDefault; - public OptionsViewModel(IMessageBus messageBus, ThemeColorRepository themeColorRepository) - { - this._messageBus = messageBus; - this._themeColorRepository = themeColorRepository; - this.IPs = new ObservableCollection(_ipList.Value); - this.SortOrders = new ObservableCollection(EnumHelpers.GetNames()); - this.Themes = new ObservableCollection(themeColorRepository.GetAll()); - this.Load(); - } + public OptionsViewModel(IMessageBus messageBus, ThemeColorRepository themeColorRepository) + { + this._messageBus = messageBus; + this._themeColorRepository = themeColorRepository; + this.IPs = new ObservableCollection(_ipList.Value); + this.SortOrders = new ObservableCollection(EnumHelpers.GetNames()); + this.Themes = new ObservableCollection(themeColorRepository.GetAll()); + this.Load(); + } - public string WindowTitle + public string WindowTitle + { + get => this._windowTitle; + set { - get => this._windowTitle; - set - { - this._windowTitle = value; - this.NotifyOfPropertyChange(() => this.WindowTitle); - } + this._windowTitle = value; + this.NotifyOfPropertyChange(() => this.WindowTitle); } + } - public string MessageListSortOrder + public string MessageListSortOrder + { + get => this._messageListSortOrder; + set { - get => this._messageListSortOrder; - set - { - this._messageListSortOrder = value; - this.NotifyOfPropertyChange(() => this.MessageListSortOrder); - } + this._messageListSortOrder = value; + this.NotifyOfPropertyChange(() => this.MessageListSortOrder); } + } - public ThemeColor ThemeColor + public ThemeColor ThemeColor + { + get => this._themeColor; + set { - get => this._themeColor; - set - { - this._themeColor = value; - this.NotifyOfPropertyChange(() => this.ThemeColor); - } + this._themeColor = value; + this.NotifyOfPropertyChange(() => this.ThemeColor); } + } - public string IP + public string IP + { + get => this._ip; + set { - get => this._ip; - set - { - this._ip = value; - this.NotifyOfPropertyChange(() => this.IP); - } + this._ip = value; + this.NotifyOfPropertyChange(() => this.IP); } + } - public int Port + public int Port + { + get => this._port; + set { - get => this._port; - set - { - this._port = value; - this.NotifyOfPropertyChange(() => this.Port); - } + this._port = value; + this.NotifyOfPropertyChange(() => this.Port); } + } - public bool RunOnStartup + public bool RunOnStartup + { + get => this._runOnStartup; + set { - get => this._runOnStartup; - set - { - this._runOnStartup = value; - this.NotifyOfPropertyChange(() => this.RunOnStartup); - } + this._runOnStartup = value; + this.NotifyOfPropertyChange(() => this.RunOnStartup); } + } - public bool MinimizeOnClose + public bool MinimizeOnClose + { + get => this._minimizeOnClose; + set { - get => this._minimizeOnClose; - set - { - this._minimizeOnClose = value; - this.NotifyOfPropertyChange(() => this.MinimizeOnClose); - } + this._minimizeOnClose = value; + this.NotifyOfPropertyChange(() => this.MinimizeOnClose); } + } - public bool MinimizeToTray + public bool MinimizeToTray + { + get => this._minimizeToTray; + set { - get => this._minimizeToTray; - set - { - this._minimizeToTray = value; - this.NotifyOfPropertyChange(() => this.MinimizeToTray); - } + this._minimizeToTray = value; + this.NotifyOfPropertyChange(() => this.MinimizeToTray); } + } - public bool ShowNotifications + public bool ShowNotifications + { + get => this._showNotifications; + set { - get => this._showNotifications; - set - { - this._showNotifications = value; - this.NotifyOfPropertyChange(() => this.ShowNotifications); - } + this._showNotifications = value; + this.NotifyOfPropertyChange(() => this.ShowNotifications); } + } - public bool StartMinimized + public bool StartMinimized + { + get => this._startMinimized; + set { - get => this._startMinimized; - set - { - this._startMinimized = value; - this.NotifyOfPropertyChange(() => this.StartMinimized); - } + this._startMinimized = value; + this.NotifyOfPropertyChange(() => this.StartMinimized); } + } - public ObservableCollection IPs { get; } + public ObservableCollection IPs { get; } - public ObservableCollection SortOrders { get; } + public ObservableCollection SortOrders { get; } - public ObservableCollection Themes { get; } + public ObservableCollection Themes { get; } - public void Load() - { - Settings.Default.CopyTo(this); + public void Load() + { + Settings.Default.CopyTo(this); - // set the theme color - this.ThemeColor = - this._themeColorRepository.FirstOrDefaultByName(Settings.Default.Theme); - } + // set the theme color + this.ThemeColor = + this._themeColorRepository.FirstOrDefaultByName(Settings.Default.Theme); + } - static IList GetIPs() - { - var ips = new List { "Any" }; + static IList GetIPs() + { + var ips = new List { "Any" }; - ips.AddRange( - Dns.GetHostAddresses("localhost") - .Select(a => a.ToString()) - .Where(NetworkHelper.IsValidIP)); + ips.AddRange( + Dns.GetHostAddresses("localhost") + .Select(a => a.ToString()) + .Where(NetworkHelper.IsValidIP)); - ips.AddRange(NetworkHelper.GetIPAddresses().Where(NetworkHelper.IsValidIP)); + ips.AddRange(NetworkHelper.GetIPAddresses().Where(NetworkHelper.IsValidIP)); - return ips; - } + return ips; + } - public async Task Save() - { - var previousSettings = new Settings(); + public async Task Save() + { + var previousSettings = new Settings(); - Settings.Default.CopyTo(previousSettings); + Settings.Default.CopyTo(previousSettings); - this.CopyTo(Settings.Default); + this.CopyTo(Settings.Default); - Settings.Default.Theme = this.ThemeColor.Name; + Settings.Default.Theme = this.ThemeColor.Name; - Settings.Default.Save(); + Settings.Default.Save(); - await this._messageBus.PublishAsync(new SettingsUpdatedEvent(previousSettings)); + await this._messageBus.PublishAsync(new SettingsUpdatedEvent(previousSettings)); - await this.TryCloseAsync(true); - } + await this.TryCloseAsync(true); } } \ No newline at end of file diff --git a/src/Papercut.UI/ViewModels/RulesConfigurationViewModel.cs b/src/Papercut.UI/ViewModels/RulesConfigurationViewModel.cs index d35b5263..aaf8e04c 100644 --- a/src/Papercut.UI/ViewModels/RulesConfigurationViewModel.cs +++ b/src/Papercut.UI/ViewModels/RulesConfigurationViewModel.cs @@ -23,66 +23,67 @@ using Papercut.AppLayer.Rules; using Papercut.Core.Domain.Rules; -namespace Papercut.ViewModels +namespace Papercut.ViewModels; + +public class RulesConfigurationViewModel : Screen { - public class RulesConfigurationViewModel : Screen - { - IRule _selectedRule; + IRule? _selectedRule; - string _windowTitle = "Rules Configuration"; + string _windowTitle = "Rules Configuration"; - public RulesConfigurationViewModel(RuleService ruleService, IEnumerable registeredRules) + public RulesConfigurationViewModel(RuleService ruleService, IEnumerable registeredRules) + { + this.RegisteredRules = new ObservableCollection(registeredRules); + this.Rules = ruleService.Rules; + this.Rules.CollectionChanged += (_, _) => { - this.RegisteredRules = new ObservableCollection(registeredRules); - this.Rules = ruleService.Rules; - this.Rules.CollectionChanged += (_, _) => + if (!this.Rules.Contains(this.SelectedRule)) { - if (!this.Rules.Contains(this.SelectedRule)) - { - this.SelectedRule = null; - } - }; - } + this.SelectedRule = null; + } + }; + } - public string WindowTitle + public string WindowTitle + { + get => this._windowTitle; + set { - get => this._windowTitle; - set - { - this._windowTitle = value; - this.NotifyOfPropertyChange(() => this.WindowTitle); - } + this._windowTitle = value; + this.NotifyOfPropertyChange(() => this.WindowTitle); } + } - public IRule SelectedRule + public IRule? SelectedRule + { + get => this._selectedRule; + set { - get => this._selectedRule; - set - { - this._selectedRule = value; - this.NotifyOfPropertyChange(() => this.SelectedRule); - this.NotifyOfPropertyChange(() => this.HasSelectedRule); - } + this._selectedRule = value; + this.NotifyOfPropertyChange(() => this.SelectedRule); + this.NotifyOfPropertyChange(() => this.HasSelectedRule); } + } - public bool HasSelectedRule => this._selectedRule != null; + public bool HasSelectedRule => this._selectedRule != null; - public ObservableCollection RegisteredRules { get; private set; } + public ObservableCollection RegisteredRules { get; private set; } - public ObservableCollection Rules { get; } + public ObservableCollection Rules { get; } - public void AddRule(IRule rule) - { - ArgumentNullException.ThrowIfNull(rule); + public void AddRule(IRule rule) + { + ArgumentNullException.ThrowIfNull(rule); - var newRule = Activator.CreateInstance(rule.GetType()) as IRule; + if (Activator.CreateInstance(rule.GetType()) is IRule newRule) + { this.Rules.Add(newRule); this.SelectedRule = newRule; } + } - public void DeleteRule() - { - if (this.SelectedRule != null) this.Rules.Remove(this.SelectedRule); - } + public void DeleteRule() + { + if (this.SelectedRule != null) this.Rules.Remove(this.SelectedRule); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/ForwardView.xaml.cs b/src/Papercut.UI/Views/ForwardView.xaml.cs index 6a34a803..fea0f83b 100644 --- a/src/Papercut.UI/Views/ForwardView.xaml.cs +++ b/src/Papercut.UI/Views/ForwardView.xaml.cs @@ -20,14 +20,13 @@ using Papercut.Helpers; -namespace Papercut.Views +namespace Papercut.Views; + +public partial class ForwardView : MetroWindow { - public partial class ForwardView : MetroWindow + public ForwardView() { - public ForwardView() - { - this.AutoAdjustBorders(); - this.InitializeComponent(); - } + this.AutoAdjustBorders(); + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/MainView.xaml b/src/Papercut.UI/Views/MainView.xaml index b44a8ef8..92265fcb 100644 --- a/src/Papercut.UI/Views/MainView.xaml +++ b/src/Papercut.UI/Views/MainView.xaml @@ -73,9 +73,11 @@ - + + + @@ -169,11 +171,10 @@ Content="https://github.com/ChangemakerStudios/Papercut-SMTP" HorizontalAlignment="Left" cal:Message.Attach="[Event MouseUp] = [Action GoToSite]" /> - - diff --git a/src/Papercut.UI/Views/MainView.xaml.cs b/src/Papercut.UI/Views/MainView.xaml.cs index 8e9b2744..5e70ab2a 100644 --- a/src/Papercut.UI/Views/MainView.xaml.cs +++ b/src/Papercut.UI/Views/MainView.xaml.cs @@ -20,14 +20,13 @@ using Papercut.Helpers; -namespace Papercut.Views +namespace Papercut.Views; + +public partial class MainView : MetroWindow { - public partial class MainView : MetroWindow + public MainView() { - public MainView() - { - this.AutoAdjustBorders(); - this.InitializeComponent(); - } + this.AutoAdjustBorders(); + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/MessageDetailBodyView.xaml.cs b/src/Papercut.UI/Views/MessageDetailBodyView.xaml.cs index 78e2ac7c..f7c44c6d 100644 --- a/src/Papercut.UI/Views/MessageDetailBodyView.xaml.cs +++ b/src/Papercut.UI/Views/MessageDetailBodyView.xaml.cs @@ -18,16 +18,15 @@ using System.Windows.Controls; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for MessageDetailBodyView.xaml +/// +public partial class MessageDetailBodyView : UserControl { - /// - /// Interaction logic for MessageDetailBodyView.xaml - /// - public partial class MessageDetailBodyView : UserControl + public MessageDetailBodyView() { - public MessageDetailBodyView() - { - this.InitializeComponent(); - } + this.InitializeComponent(); } -} +} \ No newline at end of file diff --git a/src/Papercut.UI/Views/MessageDetailHeaderView.xaml.cs b/src/Papercut.UI/Views/MessageDetailHeaderView.xaml.cs index 4b6f41f5..c5bfc2dd 100644 --- a/src/Papercut.UI/Views/MessageDetailHeaderView.xaml.cs +++ b/src/Papercut.UI/Views/MessageDetailHeaderView.xaml.cs @@ -18,16 +18,15 @@ using System.Windows.Controls; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for MessageDetailHeaderView.xaml +/// +public partial class MessageDetailHeaderView : UserControl { - /// - /// Interaction logic for MessageDetailHeaderView.xaml - /// - public partial class MessageDetailHeaderView : UserControl + public MessageDetailHeaderView() { - public MessageDetailHeaderView() - { - this.InitializeComponent(); - } + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/MessageDetailHtmlView.xaml b/src/Papercut.UI/Views/MessageDetailHtmlView.xaml index 4103c08b..31f0dd1a 100644 --- a/src/Papercut.UI/Views/MessageDetailHtmlView.xaml +++ b/src/Papercut.UI/Views/MessageDetailHtmlView.xaml @@ -8,7 +8,7 @@ mc:Ignorable="d" d:DesignHeight="400" d:DesignWidth="400" Background="White"> - + diff --git a/src/Papercut.UI/Views/MessageDetailHtmlView.xaml.cs b/src/Papercut.UI/Views/MessageDetailHtmlView.xaml.cs index 3d35f649..21f0aeb7 100644 --- a/src/Papercut.UI/Views/MessageDetailHtmlView.xaml.cs +++ b/src/Papercut.UI/Views/MessageDetailHtmlView.xaml.cs @@ -18,16 +18,15 @@ using System.Windows.Controls; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for MessageDetailHtmlView.xaml +/// +public partial class MessageDetailHtmlView : UserControl { - /// - /// Interaction logic for MessageDetailHtmlView.xaml - /// - public partial class MessageDetailHtmlView : UserControl + public MessageDetailHtmlView() { - public MessageDetailHtmlView() - { - this.InitializeComponent(); - } + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/MessageDetailPartsListView.xaml.cs b/src/Papercut.UI/Views/MessageDetailPartsListView.xaml.cs index f81f2104..f3ed6173 100644 --- a/src/Papercut.UI/Views/MessageDetailPartsListView.xaml.cs +++ b/src/Papercut.UI/Views/MessageDetailPartsListView.xaml.cs @@ -18,16 +18,15 @@ using System.Windows.Controls; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for MessageDetailPartsListView.xaml +/// +public partial class MessageDetailPartsListView : UserControl { - /// - /// Interaction logic for MessageDetailPartsListView.xaml - /// - public partial class MessageDetailPartsListView : UserControl + public MessageDetailPartsListView() { - public MessageDetailPartsListView() - { - this.InitializeComponent(); - } + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/MessageDetailRawView.xaml.cs b/src/Papercut.UI/Views/MessageDetailRawView.xaml.cs index 342a14f7..ab12f83e 100644 --- a/src/Papercut.UI/Views/MessageDetailRawView.xaml.cs +++ b/src/Papercut.UI/Views/MessageDetailRawView.xaml.cs @@ -18,16 +18,15 @@ using System.Windows.Controls; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for MessageDetailRawView.xaml +/// +public partial class MessageDetailRawView : UserControl { - /// - /// Interaction logic for MessageDetailRawView.xaml - /// - public partial class MessageDetailRawView : UserControl + public MessageDetailRawView() { - public MessageDetailRawView() - { - this.InitializeComponent(); - } + this.InitializeComponent(); } -} +} \ No newline at end of file diff --git a/src/Papercut.UI/Views/MessageDetailView.xaml.cs b/src/Papercut.UI/Views/MessageDetailView.xaml.cs index 09044317..19b2fd1f 100644 --- a/src/Papercut.UI/Views/MessageDetailView.xaml.cs +++ b/src/Papercut.UI/Views/MessageDetailView.xaml.cs @@ -18,16 +18,15 @@ using System.Windows.Controls; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for MessageDetailView.xaml +/// +public partial class MessageDetailView : UserControl { - /// - /// Interaction logic for MessageDetailView.xaml - /// - public partial class MessageDetailView : UserControl + public MessageDetailView() { - public MessageDetailView() - { - this.InitializeComponent(); - } + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/MessageListView.xaml.cs b/src/Papercut.UI/Views/MessageListView.xaml.cs index 0e8bc377..14fdbae6 100644 --- a/src/Papercut.UI/Views/MessageListView.xaml.cs +++ b/src/Papercut.UI/Views/MessageListView.xaml.cs @@ -18,16 +18,15 @@ using System.Windows.Controls; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for MessageListView.xaml +/// +public partial class MessageListView : UserControl { - /// - /// Interaction logic for MessageListView.xaml - /// - public partial class MessageListView : UserControl + public MessageListView() { - public MessageListView() - { - this.InitializeComponent(); - } + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/MimePartView.xaml.cs b/src/Papercut.UI/Views/MimePartView.xaml.cs index ebe8be2f..3e25c645 100644 --- a/src/Papercut.UI/Views/MimePartView.xaml.cs +++ b/src/Papercut.UI/Views/MimePartView.xaml.cs @@ -20,17 +20,16 @@ using Papercut.Helpers; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for MimePartView.xaml +/// +public partial class MimePartView : MetroWindow { - /// - /// Interaction logic for MimePartView.xaml - /// - public partial class MimePartView : MetroWindow + public MimePartView() { - public MimePartView() - { - this.AutoAdjustBorders(); - this.InitializeComponent(); - } + this.AutoAdjustBorders(); + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/OptionsView.xaml.cs b/src/Papercut.UI/Views/OptionsView.xaml.cs index d51a9809..b84d3f3f 100644 --- a/src/Papercut.UI/Views/OptionsView.xaml.cs +++ b/src/Papercut.UI/Views/OptionsView.xaml.cs @@ -20,14 +20,13 @@ using Papercut.Helpers; -namespace Papercut.Views +namespace Papercut.Views; + +public partial class OptionsView : MetroWindow { - public partial class OptionsView : MetroWindow + public OptionsView() { - public OptionsView() - { - this.AutoAdjustBorders(); - this.InitializeComponent(); - } + this.AutoAdjustBorders(); + this.InitializeComponent(); } } \ No newline at end of file diff --git a/src/Papercut.UI/Views/RulesConfigurationView.xaml.cs b/src/Papercut.UI/Views/RulesConfigurationView.xaml.cs index 39c8ebf3..0487ffc7 100644 --- a/src/Papercut.UI/Views/RulesConfigurationView.xaml.cs +++ b/src/Papercut.UI/Views/RulesConfigurationView.xaml.cs @@ -20,17 +20,16 @@ using Papercut.Helpers; -namespace Papercut.Views +namespace Papercut.Views; + +/// +/// Interaction logic for RulesConfigurationView.xaml +/// +public partial class RulesConfigurationView : MetroWindow { - /// - /// Interaction logic for RulesConfigurationView.xaml - /// - public partial class RulesConfigurationView : MetroWindow + public RulesConfigurationView() { - public RulesConfigurationView() - { - this.AutoAdjustBorders(); - this.InitializeComponent(); - } + this.AutoAdjustBorders(); + this.InitializeComponent(); } } \ No newline at end of file