diff --git a/src/Papercut.Core/Domain/Rules/IRule.cs b/src/Papercut.Core/Domain/Rules/IRule.cs index d86a6fc6..d00b4fa9 100644 --- a/src/Papercut.Core/Domain/Rules/IRule.cs +++ b/src/Papercut.Core/Domain/Rules/IRule.cs @@ -28,6 +28,8 @@ public interface IRule : INotifyPropertyChanged { Guid Id { get; } + string Name { get; set; } + bool IsEnabled { get; set; } string Type { get; } diff --git a/src/Papercut.Rules/App/RulesRunner.cs b/src/Papercut.Rules/App/RulesRunner.cs index 3e5ca182..2ec9f058 100644 --- a/src/Papercut.Rules/App/RulesRunner.cs +++ b/src/Papercut.Rules/App/RulesRunner.cs @@ -61,6 +61,8 @@ public async Task RunNewMessageRules(INewMessageRule[] rules, MessageEntry messa if (rules == null) throw new ArgumentNullException(nameof(rules)); if (messageEntry == null) throw new ArgumentNullException(nameof(messageEntry)); + token.ThrowIfCancellationRequested(); + var ruleTasks = new List(); foreach (var rule in rules.Where(r => r.IsEnabled)) @@ -125,6 +127,10 @@ async Task DispatchRuleAsync(TRule rule, MessageEntry? messageEntry, Canc var ruleDispatcher = _lifetimeScope.Resolve>(); await ruleDispatcher.DispatchAsync(rule, messageEntry, token); } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { _logger.Warning( diff --git a/src/Papercut.Rules/Domain/Rules/NewMessageRuleBase.cs b/src/Papercut.Rules/Domain/Rules/NewMessageRuleBase.cs index ed0145b8..0e872bb9 100644 --- a/src/Papercut.Rules/Domain/Rules/NewMessageRuleBase.cs +++ b/src/Papercut.Rules/Domain/Rules/NewMessageRuleBase.cs @@ -31,9 +31,25 @@ public abstract class NewMessageRuleBase : INewMessageRule { bool _isEnabled; + string _name; + [Category("Information")] public Guid Id { get; protected set; } = Guid.NewGuid(); + [Category("Information")] + [DisplayName("Name")] + [Description("A friendly name for this rule")] + public string Name + { + get => this._name; + set + { + if (value == this._name) return; + this._name = value; + this.OnPropertyChanged(nameof(this.Name)); + } + } + [Category("State")] [Browsable(true)] [DisplayName("Is Enabled")] @@ -59,7 +75,7 @@ public virtual bool IsEnabled public virtual string Description => this.GetPropertiesForDescription() - .Where(s => !s.Key.IsAny("Id", "Type", "Description")) + .Where(s => !s.Key.IsAny("Id", "Name", "Type", "Description")) .OrderBy(s => s.Key) .ToFormattedPairs() .Join("\r\n"); diff --git a/src/Papercut.Rules/Domain/Rules/PeriodicBackgroundRuleBase.cs b/src/Papercut.Rules/Domain/Rules/PeriodicBackgroundRuleBase.cs index 876e9875..7f1f9fbd 100644 --- a/src/Papercut.Rules/Domain/Rules/PeriodicBackgroundRuleBase.cs +++ b/src/Papercut.Rules/Domain/Rules/PeriodicBackgroundRuleBase.cs @@ -31,9 +31,25 @@ public abstract class PeriodicBackgroundRuleBase : IPeriodicBackgroundRule { bool _isEnabled; + string _name; + [Category("Information")] public Guid Id { get; protected set; } = Guid.NewGuid(); + [Category("Information")] + [DisplayName("Name")] + [Description("A friendly name for this rule")] + public string Name + { + get => this._name; + set + { + if (value == this._name) return; + this._name = value; + this.OnPropertyChanged(nameof(this.Name)); + } + } + [Category("State")] [Browsable(true)] [DisplayName("Is Enabled")] @@ -59,7 +75,7 @@ public virtual bool IsEnabled public virtual string Description => this.GetPropertiesForDescription() - .Where(s => !s.Key.IsAny("Id", "Type", "Description")) + .Where(s => !s.Key.IsAny("Id", "Name", "Type", "Description")) .OrderBy(s => s.Key) .ToFormattedPairs() .Join("\r\n"); diff --git a/src/Papercut.UI/Properties/Settings.Designer.cs b/src/Papercut.UI/Properties/Settings.Designer.cs index 1fb25658..5cfdc8c0 100644 --- a/src/Papercut.UI/Properties/Settings.Designer.cs +++ b/src/Papercut.UI/Properties/Settings.Designer.cs @@ -95,6 +95,54 @@ public string ForwardTo { } } + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string ForwardSmtpUsername { + get { + return ((string)(this["ForwardSmtpUsername"])); + } + set { + this["ForwardSmtpUsername"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string ForwardSmtpPassword { + get { + return ((string)(this["ForwardSmtpPassword"])); + } + set { + this["ForwardSmtpPassword"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("25")] + public int ForwardSmtpPort { + get { + return ((int)(this["ForwardSmtpPort"])); + } + set { + this["ForwardSmtpPort"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool ForwardSmtpUseSsl { + get { + return ((bool)(this["ForwardSmtpUseSsl"])); + } + set { + this["ForwardSmtpUseSsl"] = value; + } + } + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("True")] diff --git a/src/Papercut.UI/Properties/Settings.settings b/src/Papercut.UI/Properties/Settings.settings index 6a08f3ae..411bbb30 100644 --- a/src/Papercut.UI/Properties/Settings.settings +++ b/src/Papercut.UI/Properties/Settings.settings @@ -20,6 +20,18 @@ + + + + + + + + 25 + + + False + True diff --git a/src/Papercut.UI/ViewModels/ForwardViewModel.cs b/src/Papercut.UI/ViewModels/ForwardViewModel.cs index 5a780825..28399038 100644 --- a/src/Papercut.UI/ViewModels/ForwardViewModel.cs +++ b/src/Papercut.UI/ViewModels/ForwardViewModel.cs @@ -1,14 +1,14 @@ -// Papercut -// +// Papercut +// // Copyright © 2008 - 2012 Ken Robertson // Copyright © 2013 - 2025 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. @@ -16,10 +16,15 @@ // limitations under the License. +using System.Collections.ObjectModel; using System.Text.RegularExpressions; +using Papercut.AppLayer.Rules; using Papercut.Core; +using Papercut.Core.Domain.Rules; using Papercut.Domain.UiCommands; +using Papercut.Rules.Domain.Forwarding; +using Papercut.Rules.Domain.Relaying; namespace Papercut.ViewModels; @@ -32,6 +37,8 @@ public class ForwardViewModel : Screen readonly IUiCommandHub _uiCommandHub; + readonly RuleService _ruleService; + string _from; bool _fromSetting; @@ -40,11 +47,25 @@ public class ForwardViewModel : Screen string _to; + string _username; + + string _password; + + int _port = 25; + + bool _useSsl; + + bool _useAuthentication; + string _windowTitle = "Forward Message"; - public ForwardViewModel(IUiCommandHub uiCommandHub) + RelayRuleListItem _selectedRule; + + public ForwardViewModel(IUiCommandHub uiCommandHub, RuleService ruleService) { _uiCommandHub = uiCommandHub; + _ruleService = ruleService; + AvailableRules = new ObservableCollection(); } public bool FromSetting @@ -97,12 +118,122 @@ public string From } } + public string Username + { + get => this._username; + set + { + this._username = value; + this.NotifyOfPropertyChange(() => this.Username); + } + } + + public string Password + { + get => this._password; + set + { + this._password = value; + this.NotifyOfPropertyChange(() => this.Password); + } + } + + public int Port + { + get => this._port; + set + { + this._port = value; + this.NotifyOfPropertyChange(() => this.Port); + } + } + + public bool UseSsl + { + get => this._useSsl; + set + { + this._useSsl = value; + this.NotifyOfPropertyChange(() => this.UseSsl); + } + } + + public bool UseAuthentication + { + get => this._useAuthentication; + set + { + this._useAuthentication = value; + this.NotifyOfPropertyChange(() => this.UseAuthentication); + + if (!value) + { + this.Username = string.Empty; + this.Password = string.Empty; + } + } + } + + public ObservableCollection AvailableRules { get; } + + public bool HasAvailableRules => AvailableRules.Count > 0; + + public RelayRuleListItem SelectedRule + { + get => this._selectedRule; + set + { + this._selectedRule = value; + this.NotifyOfPropertyChange(() => this.SelectedRule); + + if (value?.Rule != null) + { + PopulateFromRule(value.Rule); + } + } + } + + void PopulateFromRule(RelayRule rule) + { + this.Server = rule.SmtpServer; + this.Port = rule.SmtpPort; + this.UseSsl = rule.SmtpUseSSL; + + var hasCredentials = !string.IsNullOrEmpty(rule.SmtpUsername) || !string.IsNullOrEmpty(rule.SmtpPassword); + this.UseAuthentication = hasCredentials; + this.Username = rule.SmtpUsername; + this.Password = rule.SmtpPassword; + + if (rule is ForwardRule forwardRule) + { + this.From = forwardRule.FromEmail; + this.To = forwardRule.ToEmail; + } + } + + void LoadAvailableRules() + { + AvailableRules.Clear(); + + var relayRules = _ruleService.Rules.OfType(); + foreach (var rule in relayRules) + { + AvailableRules.Add(new RelayRuleListItem(rule)); + } + + this.NotifyOfPropertyChange(() => this.HasAvailableRules); + } + void Load() { // Load previous settings this.Server = Settings.Default.ForwardServer; this.To = Settings.Default.ForwardTo; this.From = Settings.Default.ForwardFrom; + this.Username = Settings.Default.ForwardSmtpUsername; + // Password is intentionally not persisted to settings for security + this.Port = Settings.Default.ForwardSmtpPort; + this.UseSsl = Settings.Default.ForwardSmtpUseSsl; } public async Task Cancel() @@ -114,6 +245,8 @@ protected override void OnViewLoaded(object view) { base.OnViewLoaded(view); + LoadAvailableRules(); + if (this.FromSetting) this.Load(); } @@ -136,15 +269,55 @@ public async Task Send() return; } + if (this.Port < 1 || this.Port > 65535) + { + _uiCommandHub.ShowMessage( + "SMTP port must be between 1 and 65535.", + AppConstants.ApplicationName); + 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.ForwardSmtpUsername = this.Username?.Trim() ?? string.Empty; + // Password is intentionally not persisted to settings for security + Settings.Default.ForwardSmtpPort = this.Port; + Settings.Default.ForwardSmtpUseSsl = this.UseSsl; Settings.Default.Save(); } await this.TryCloseAsync(true); } -} \ No newline at end of file +} + +public class RelayRuleListItem +{ + public RelayRuleListItem(RelayRule rule) + { + Rule = rule; + } + + public RelayRule Rule { get; } + + public string DisplayName + { + get + { + if (!string.IsNullOrWhiteSpace(Rule.Name)) + return Rule.Name; + + var type = Rule.Type; + var server = Rule.SmtpServer; + if (Rule is ForwardRule fwd && !string.IsNullOrWhiteSpace(fwd.ToEmail)) + return $"{type} - {server} -> {fwd.ToEmail}"; + + return $"{type} - {server}"; + } + } + + public override string ToString() => DisplayName; +} diff --git a/src/Papercut.UI/ViewModels/MainViewModel.cs b/src/Papercut.UI/ViewModels/MainViewModel.cs index b522b55d..f8bc9272 100644 --- a/src/Papercut.UI/ViewModels/MainViewModel.cs +++ b/src/Papercut.UI/ViewModels/MainViewModel.cs @@ -36,7 +36,6 @@ using Papercut.Infrastructure.Resources; using Papercut.Infrastructure.WebView; using Papercut.Rules.App.Forwarding; -using Papercut.Rules.App.Relaying; using Papercut.Rules.Domain.Forwarding; using Papercut.Views; @@ -697,11 +696,14 @@ public async Task ForwardSelected() var forwardRule = new ForwardRule { FromEmail = forwardViewModel.From, - ToEmail = forwardViewModel.To + ToEmail = forwardViewModel.To, + SmtpServer = forwardViewModel.Server?.Trim() ?? string.Empty, + SmtpPort = forwardViewModel.Port, + SmtpUseSSL = forwardViewModel.UseSsl, + SmtpUsername = forwardViewModel.Username ?? string.Empty, + SmtpPassword = forwardViewModel.Password ?? string.Empty }; - forwardRule.PopulateServerFromUri(forwardViewModel.Server); - // send message using relay dispatcher... await this._forwardRuleDispatch.DispatchAsync( forwardRule, diff --git a/src/Papercut.UI/Views/ForwardView.xaml b/src/Papercut.UI/Views/ForwardView.xaml index 2d8c5bd7..27266cc3 100644 --- a/src/Papercut.UI/Views/ForwardView.xaml +++ b/src/Papercut.UI/Views/ForwardView.xaml @@ -1,4 +1,4 @@ - - + + + + -