From 60aff5ea69aa19c6fb9afa8573fd5f77ab40de3a Mon Sep 17 00:00:00 2001 From: Rob Wood Date: Fri, 19 Apr 2024 21:34:37 +0100 Subject: [PATCH] Add static list of users (#1413) --- Rnwood.Smtp4dev/ApiModel/Server.cs | 4 +- .../ClientApp/src/ApiClient/Server.ts | 7 ++- .../ClientApp/src/ApiClient/User.ts | 13 ++++++ .../src/components/settingsdialog.vue | 45 +++++++++++++++++-- Rnwood.Smtp4dev/CommandLineOptions.cs | 2 +- Rnwood.Smtp4dev/CommandLineParser.cs | 2 +- .../Controllers/ClientSettingsController.cs | 2 +- .../Controllers/ServerController.cs | 5 ++- Rnwood.Smtp4dev/Program.cs | 1 + Rnwood.Smtp4dev/Rnwood.Smtp4dev.csproj | 1 + Rnwood.Smtp4dev/Server/CertificateHelper.cs | 1 + Rnwood.Smtp4dev/Server/ImapServer.cs | 33 ++++++++++++-- Rnwood.Smtp4dev/Server/ScriptingHost.cs | 13 +++--- .../Server/{ => Settings}/ClientOptions.cs | 2 +- .../Server/{ => Settings}/RelayOptions.cs | 6 +-- .../Server/{ => Settings}/ServerOptions.cs | 4 +- Rnwood.Smtp4dev/Server/Settings/User.cs | 8 ++++ Rnwood.Smtp4dev/Server/Smtp4devServer.cs | 41 +++++++++++++---- .../Service/HostingEnvironmentHelper.cs | 2 +- Rnwood.Smtp4dev/Startup.cs | 1 + Rnwood.Smtp4dev/appsettings.Development.json | 10 ++++- Rnwood.Smtp4dev/appsettings.json | 14 +++++- .../Extensions/Auth/AuthMechanisms.cs | 1 - .../Auth/CramMd5AuthenticationCredentials.cs | 2 +- .../Auth/IAuthenticationCredentials.cs | 1 + ...ationCredentialsCanValidateWithPassword.cs | 13 ++++++ .../Rnwood.SmtpServer/Rnwood.SmtpServer.xml | 12 ++--- smtpserver/Rnwood.SmtpServer/ServerOptions.cs | 25 ++++++----- 28 files changed, 213 insertions(+), 58 deletions(-) create mode 100644 Rnwood.Smtp4dev/ClientApp/src/ApiClient/User.ts rename Rnwood.Smtp4dev/Server/{ => Settings}/ClientOptions.cs (80%) rename Rnwood.Smtp4dev/Server/{ => Settings}/RelayOptions.cs (91%) rename Rnwood.Smtp4dev/Server/{ => Settings}/ServerOptions.cs (93%) create mode 100644 Rnwood.Smtp4dev/Server/Settings/User.cs create mode 100644 smtpserver/Rnwood.SmtpServer/Extensions/Auth/IAuthenticationCredentialsCanValidateWithPassword.cs diff --git a/Rnwood.Smtp4dev/ApiModel/Server.cs b/Rnwood.Smtp4dev/ApiModel/Server.cs index 89073dae6..b13560269 100644 --- a/Rnwood.Smtp4dev/ApiModel/Server.cs +++ b/Rnwood.Smtp4dev/ApiModel/Server.cs @@ -1,4 +1,5 @@ -using System; +using Rnwood.Smtp4dev.Server.Settings; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -34,6 +35,7 @@ public class Server public string RecipientValidationExpression { get; set; } public string MessageValidationExpression { get; set; } public bool DisableIPv6 { get; set; } + public User[] Users { get; set; } } } diff --git a/Rnwood.Smtp4dev/ClientApp/src/ApiClient/Server.ts b/Rnwood.Smtp4dev/ClientApp/src/ApiClient/Server.ts index 374c43fb2..bcf13e950 100644 --- a/Rnwood.Smtp4dev/ClientApp/src/ApiClient/Server.ts +++ b/Rnwood.Smtp4dev/ClientApp/src/ApiClient/Server.ts @@ -1,11 +1,12 @@  //ServerRelayOptions from Server import ServerRelayOptions from './ServerRelayOptions'; +import User from './User'; export default class Server { - + constructor(isRunning: boolean, exception: string, portNumber: number, hostName: string, allowRemoteConnections: boolean, numberOfMessagesToKeep: number, numberOfSessionsToKeep: number, relayOptions: ServerRelayOptions, imapPortNumber: number, settingsAreEditable: boolean, disableMessageSanitisation: boolean, automaticRelayExpression: string, tlsMode: string, credentialsValidationExpression: string, authenticationRequired: boolean, - secureConnectionRequired: boolean, recipientValidationExpression: string, messageValidationExpression: string, disableIPv6: string) { + secureConnectionRequired: boolean, recipientValidationExpression: string, messageValidationExpression: string, disableIPv6: string, users: User[]) { this.isRunning = isRunning; this.exception = exception; @@ -26,6 +27,7 @@ export default class Server { this.recipientValidationExpression = recipientValidationExpression; this.messageValidationExpression = messageValidationExpression; this.disableIPv6 = disableIPv6; + this.users = users; } @@ -48,4 +50,5 @@ export default class Server { recipientValidationExpression: string; messageValidationExpression: string; disableIPv6: string; + users: User[]; } diff --git a/Rnwood.Smtp4dev/ClientApp/src/ApiClient/User.ts b/Rnwood.Smtp4dev/ClientApp/src/ApiClient/User.ts new file mode 100644 index 000000000..fff177950 --- /dev/null +++ b/Rnwood.Smtp4dev/ClientApp/src/ApiClient/User.ts @@ -0,0 +1,13 @@ + +export default class User { + username: string; + password: string; + + constructor(username: string, password: string) { + + this.username = username; + this.password = password; + } + + +} diff --git a/Rnwood.Smtp4dev/ClientApp/src/components/settingsdialog.vue b/Rnwood.Smtp4dev/ClientApp/src/components/settingsdialog.vue index 8f45c4dc3..b4d2b1fd2 100644 --- a/Rnwood.Smtp4dev/ClientApp/src/components/settingsdialog.vue +++ b/Rnwood.Smtp4dev/ClientApp/src/components/settingsdialog.vue @@ -18,6 +18,10 @@ + + + + @@ -49,10 +53,6 @@ - - - - @@ -126,6 +126,33 @@ New Auto-Relay Recipient + + + + + SMTP and IMAP Users: + + +
+ + + + + + + + + + + + Remove + + +
+ New User + + +
@@ -182,6 +209,16 @@ } + checkUsernameUnique(rule: any, value: any, callback: any) { + if (this.server?.users.filter(u => u != value).find(u=> u.username == value.username)) { + + callback(new Error('Username must be unique')); + } else { + callback(); + } + + } + @Prop() visible: boolean = false; diff --git a/Rnwood.Smtp4dev/CommandLineOptions.cs b/Rnwood.Smtp4dev/CommandLineOptions.cs index cc2cb592b..c21997780 100644 --- a/Rnwood.Smtp4dev/CommandLineOptions.cs +++ b/Rnwood.Smtp4dev/CommandLineOptions.cs @@ -1,4 +1,4 @@ -using Rnwood.Smtp4dev.Server; +using Rnwood.Smtp4dev.Server.Settings; namespace Rnwood.Smtp4dev { diff --git a/Rnwood.Smtp4dev/CommandLineParser.cs b/Rnwood.Smtp4dev/CommandLineParser.cs index 2a9c2008e..c1fa64411 100644 --- a/Rnwood.Smtp4dev/CommandLineParser.cs +++ b/Rnwood.Smtp4dev/CommandLineParser.cs @@ -46,7 +46,7 @@ public static MapOptions TryParseCommandLine(IEnumerable map.Add(data, x=> x.InstallPath) }, { "disablemessagesanitisation", "Disables message HTML sanitisation.", data => map.Add((data !=null).ToString(), x=> x.ServerOptions.DisableMessageSanitisation) }, { "applicationName=","", data => map.Add(data, x => x.ApplicationName), true}, - { "authenticationrequired", "Requires that SMTP clients authenticate", data => map.Add((data !=null).ToString(), x=> x.ServerOptions.AuthenticationRequired) }, + { "authenticationrequired", "Requires that SMTP and IMAP clients authenticate", data => map.Add((data !=null).ToString(), x=> x.ServerOptions.AuthenticationRequired) }, { "secureconnectionrequired", "Requires that SMTP clients use SSL/TLS", data => map.Add((data !=null).ToString(), x=> x.ServerOptions.SecureConnectionRequired) } }; diff --git a/Rnwood.Smtp4dev/Controllers/ClientSettingsController.cs b/Rnwood.Smtp4dev/Controllers/ClientSettingsController.cs index e50b0b729..927a81a2d 100644 --- a/Rnwood.Smtp4dev/Controllers/ClientSettingsController.cs +++ b/Rnwood.Smtp4dev/Controllers/ClientSettingsController.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Options; using NSwag.Annotations; using Rnwood.Smtp4dev.ApiModel; -using Rnwood.Smtp4dev.Server; +using Rnwood.Smtp4dev.Server.Settings; namespace Rnwood.Smtp4dev.Controllers { diff --git a/Rnwood.Smtp4dev/Controllers/ServerController.cs b/Rnwood.Smtp4dev/Controllers/ServerController.cs index 470cec729..22e27610e 100644 --- a/Rnwood.Smtp4dev/Controllers/ServerController.cs +++ b/Rnwood.Smtp4dev/Controllers/ServerController.cs @@ -10,6 +10,7 @@ using System.Text.Json.Serialization; using NSwag.Annotations; using System.ComponentModel; +using Rnwood.Smtp4dev.Server.Settings; namespace Rnwood.Smtp4dev.Controllers { @@ -70,7 +71,8 @@ public ApiModel.Server GetServer() CredentialsValidationExpression = serverOptions.CurrentValue.CredentialsValidationExpression, RecipientValidationExpression = serverOptions.CurrentValue.RecipientValidationExpression, MessageValidationExpression = serverOptions.CurrentValue.MessageValidationExpression, - DisableIPv6 = serverOptions.CurrentValue.DisableIPv6 + DisableIPv6 = serverOptions.CurrentValue.DisableIPv6, + Users = serverOptions.CurrentValue.Users }; } @@ -105,6 +107,7 @@ public ActionResult UpdateServer(ApiModel.Server serverUpdate) newSettings.RecipientValidationExpression = serverUpdate.RecipientValidationExpression; newSettings.MessageValidationExpression = serverUpdate.MessageValidationExpression; newSettings.DisableIPv6 = serverUpdate.DisableIPv6; + newSettings.Users = serverUpdate.Users; newRelaySettings.SmtpServer = serverUpdate.RelayOptions.SmtpServer; newRelaySettings.SmtpPort = serverUpdate.RelayOptions.SmtpPort; diff --git a/Rnwood.Smtp4dev/Program.cs b/Rnwood.Smtp4dev/Program.cs index 49b122e01..cdf9d7b65 100644 --- a/Rnwood.Smtp4dev/Program.cs +++ b/Rnwood.Smtp4dev/Program.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Hosting; using Mono.Options; using Rnwood.Smtp4dev.Server; +using Rnwood.Smtp4dev.Server.Settings; using Rnwood.Smtp4dev.Service; using Serilog; diff --git a/Rnwood.Smtp4dev/Rnwood.Smtp4dev.csproj b/Rnwood.Smtp4dev/Rnwood.Smtp4dev.csproj index 251bace31..6b89e0ab7 100644 --- a/Rnwood.Smtp4dev/Rnwood.Smtp4dev.csproj +++ b/Rnwood.Smtp4dev/Rnwood.Smtp4dev.csproj @@ -173,6 +173,7 @@ + diff --git a/Rnwood.Smtp4dev/Server/CertificateHelper.cs b/Rnwood.Smtp4dev/Server/CertificateHelper.cs index 52a5a78dd..9af1692a1 100644 --- a/Rnwood.Smtp4dev/Server/CertificateHelper.cs +++ b/Rnwood.Smtp4dev/Server/CertificateHelper.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; +using Rnwood.Smtp4dev.Server.Settings; using Serilog; namespace Rnwood.Smtp4dev.Server diff --git a/Rnwood.Smtp4dev/Server/ImapServer.cs b/Rnwood.Smtp4dev/Server/ImapServer.cs index 4d8afb38e..f89d57e98 100644 --- a/Rnwood.Smtp4dev/Server/ImapServer.cs +++ b/Rnwood.Smtp4dev/Server/ImapServer.cs @@ -20,15 +20,17 @@ using System.Threading; using Microsoft.AspNetCore.Http; using Org.BouncyCastle.Utilities.Net; +using Rnwood.Smtp4dev.Server.Settings; namespace Rnwood.Smtp4dev.Server { public class ImapServer : IHostedService { - public ImapServer(IOptionsMonitor serverOptions, IServiceScopeFactory serviceScopeFactory) + public ImapServer(IOptionsMonitor serverOptions, ScriptingHost scriptingHost, IServiceScopeFactory serviceScopeFactory) { this.serverOptions = serverOptions; this.serviceScopeFactory = serviceScopeFactory; + this.scriptingHost = scriptingHost; IDisposable eventHandler = null; var obs = Observable.FromEvent(e => eventHandler = serverOptions.OnChange(e), e => eventHandler.Dispose()); @@ -91,7 +93,7 @@ public async void TryStart() Bindings = bindings.ToArray(), GreetingText = "smtp4dev" }; - imapServer.SessionCreated += (o, ea) => new SessionHandler(ea.Session, this.serviceScopeFactory); + imapServer.SessionCreated += (o, ea) => new SessionHandler(ea.Session, scriptingHost, serverOptions, this.serviceScopeFactory); var errorTcs = new TaskCompletionSource(); @@ -102,6 +104,7 @@ public async void TryStart() errorTcs.TrySetResult(ea); } }; + var startedTcs = new TaskCompletionSource(); imapServer.Started += (s, ea) => startedTcs.SetResult(ea); @@ -152,6 +155,7 @@ public void Stop() private IMAP_Server imapServer; private IOptionsMonitor serverOptions; private readonly IServiceScopeFactory serviceScopeFactory; + private readonly ScriptingHost scriptingHost; private readonly ILogger log = Log.ForContext(); private void Logger_WriteLog(object sender, LumiSoft.Net.Log.WriteLogEventArgs e) @@ -171,7 +175,8 @@ Task IHostedService.StopAsync(CancellationToken cancellationToken) class SessionHandler { - public SessionHandler(IMAP_Session session, IServiceScopeFactory serviceScopeFactory) + private readonly ILogger log = Log.ForContext(); + public SessionHandler(IMAP_Session session, ScriptingHost scriptingHost, IOptionsMonitor serverOptions, IServiceScopeFactory serviceScopeFactory) { this.session = session; session.List += Session_List; @@ -182,6 +187,8 @@ public SessionHandler(IMAP_Session session, IServiceScopeFactory serviceScopeFac session.Capabilities.Remove("NAMESPACE"); session.Store += Session_Store; session.Select += Session_Select; + this.scriptingHost = scriptingHost; + this.serverOptions = serverOptions; this.serviceScopeFactory = serviceScopeFactory; } @@ -220,6 +227,8 @@ private void Session_Store(object sender, IMAP_e_Store e) } } + private readonly ScriptingHost scriptingHost; + private readonly IOptionsMonitor serverOptions; private readonly IServiceScopeFactory serviceScopeFactory; private readonly IMAP_Session session; @@ -268,7 +277,23 @@ private void Session_Fetch(object sender, IMAP_e_Fetch e) private void Session_Login(object sender, IMAP_e_Login e) { - e.IsAuthenticated = true; + if (!serverOptions.CurrentValue.AuthenticationRequired) + { + e.IsAuthenticated = true; + return; + } + + var user = serverOptions.CurrentValue.Users?.FirstOrDefault(u => e.UserName.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase)); + + if (user != null && e.Password.Equals( user.Password, StringComparison.CurrentCultureIgnoreCase)) + { + log.Information("IMAP login success for user {user}", e.UserName); + e.IsAuthenticated = true; + } else + { + log.Error("IMAP login failure for user {user}", e.UserName); + e.IsAuthenticated = false; + } } private void Session_List(object sender, IMAP_e_List e) diff --git a/Rnwood.Smtp4dev/Server/ScriptingHost.cs b/Rnwood.Smtp4dev/Server/ScriptingHost.cs index 21989bdcc..7e49cc11a 100644 --- a/Rnwood.Smtp4dev/Server/ScriptingHost.cs +++ b/Rnwood.Smtp4dev/Server/ScriptingHost.cs @@ -10,22 +10,23 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Options; using Rnwood.Smtp4dev.DbModel; +using Rnwood.Smtp4dev.Server.Settings; using Rnwood.SmtpServer; using Rnwood.SmtpServer.Extensions.Auth; using Serilog; namespace Rnwood.Smtp4dev.Server; -internal class ScriptingHost +public class ScriptingHost { private readonly ILogger log = Log.ForContext(); private IOptionsMonitor relayOptions; - private IOptionsMonitor serverOptions; + private IOptionsMonitor serverOptions; - public ScriptingHost(IOptionsMonitor relayOptions, IOptionsMonitor serverOptions) + public ScriptingHost(IOptionsMonitor relayOptions, IOptionsMonitor serverOptions) { this.relayOptions = relayOptions; this.serverOptions = serverOptions; @@ -55,7 +56,7 @@ private void ParseScript(string type, string expression, ref Script script, ref } } - private void ParseScripts(RelayOptions relayOptionsCurrentValue, ServerOptions serverOptionsCurrentValue) + private void ParseScripts(RelayOptions relayOptionsCurrentValue, Settings.ServerOptions serverOptionsCurrentValue) { ParseScript("AutomaticRelayExpression", relayOptionsCurrentValue.AutomaticRelayExpression, ref shouldRelayScript, ref shouldRelaySource); ParseScript("CredentialsValidationExpression", serverOptionsCurrentValue.CredentialsValidationExpression, ref credValidationScript, @@ -135,11 +136,11 @@ public IReadOnlyCollection GetAutoRelayRecipients(ApiModel.Message messa } - public AuthenticationResult ValidateCredentials(ApiModel.Session session, IAuthenticationCredentials credentials) + public AuthenticationResult? ValidateCredentials(ApiModel.Session session, IAuthenticationCredentials credentials) { if (credValidationScript == null) { - return AuthenticationResult.Success; + return null; } Engine jsEngine = new Engine(); diff --git a/Rnwood.Smtp4dev/Server/ClientOptions.cs b/Rnwood.Smtp4dev/Server/Settings/ClientOptions.cs similarity index 80% rename from Rnwood.Smtp4dev/Server/ClientOptions.cs rename to Rnwood.Smtp4dev/Server/Settings/ClientOptions.cs index 0dd14f4f7..f54670290 100644 --- a/Rnwood.Smtp4dev/Server/ClientOptions.cs +++ b/Rnwood.Smtp4dev/Server/Settings/ClientOptions.cs @@ -1,4 +1,4 @@ -namespace Rnwood.Smtp4dev.Server +namespace Rnwood.Smtp4dev.Server.Settings { public class ClientOptions { diff --git a/Rnwood.Smtp4dev/Server/RelayOptions.cs b/Rnwood.Smtp4dev/Server/Settings/RelayOptions.cs similarity index 91% rename from Rnwood.Smtp4dev/Server/RelayOptions.cs rename to Rnwood.Smtp4dev/Server/Settings/RelayOptions.cs index a6e59442a..d320e6a4e 100644 --- a/Rnwood.Smtp4dev/Server/RelayOptions.cs +++ b/Rnwood.Smtp4dev/Server/Settings/RelayOptions.cs @@ -2,7 +2,7 @@ using MailKit.Security; using System.Text.Json.Serialization; -namespace Rnwood.Smtp4dev.Server +namespace Rnwood.Smtp4dev.Server.Settings { public class RelayOptions { @@ -24,7 +24,7 @@ public int SmtpPort public SecureSocketOptions TlsMode { get; set; } = SecureSocketOptions.Auto; public string[] AutomaticEmails { get; set; } = System.Array.Empty(); - + public string AutomaticRelayExpression { get; set; } public string SenderAddress { get; set; } = ""; @@ -37,7 +37,7 @@ public int SmtpPort public string AutomaticEmailsString { get => string.Join(",", AutomaticEmails); - set => this.AutomaticEmails = value.Split(','); + set => AutomaticEmails = value.Split(','); } } } \ No newline at end of file diff --git a/Rnwood.Smtp4dev/Server/ServerOptions.cs b/Rnwood.Smtp4dev/Server/Settings/ServerOptions.cs similarity index 93% rename from Rnwood.Smtp4dev/Server/ServerOptions.cs rename to Rnwood.Smtp4dev/Server/Settings/ServerOptions.cs index f0b760198..0a39fcd20 100644 --- a/Rnwood.Smtp4dev/Server/ServerOptions.cs +++ b/Rnwood.Smtp4dev/Server/Settings/ServerOptions.cs @@ -1,7 +1,7 @@ using System.Net; using Esprima.Ast; -namespace Rnwood.Smtp4dev.Server +namespace Rnwood.Smtp4dev.Server.Settings { public class ServerOptions { @@ -38,5 +38,7 @@ public class ServerOptions public string MessageValidationExpression { get; set; } public bool DisableIPv6 { get; set; } = false; + + public User[] Users { get; set; } = new User[0]; } } diff --git a/Rnwood.Smtp4dev/Server/Settings/User.cs b/Rnwood.Smtp4dev/Server/Settings/User.cs new file mode 100644 index 000000000..7feebd55e --- /dev/null +++ b/Rnwood.Smtp4dev/Server/Settings/User.cs @@ -0,0 +1,8 @@ +namespace Rnwood.Smtp4dev.Server.Settings +{ + public class User + { + public string Username { get; set; } + public string Password { get; set; } + } +} diff --git a/Rnwood.Smtp4dev/Server/Smtp4devServer.cs b/Rnwood.Smtp4dev/Server/Smtp4devServer.cs index 49380b526..0018c442d 100644 --- a/Rnwood.Smtp4dev/Server/Smtp4devServer.cs +++ b/Rnwood.Smtp4dev/Server/Smtp4devServer.cs @@ -26,6 +26,7 @@ using Microsoft.Extensions.Hosting; using System.Threading; using System.Net; +using Rnwood.Smtp4dev.Server.Settings; namespace Rnwood.Smtp4dev.Server { @@ -33,7 +34,7 @@ internal class Smtp4devServer : ISmtp4devServer, IHostedService { private readonly ILogger log = Log.ForContext(); - public Smtp4devServer(IServiceScopeFactory serviceScopeFactory, IOptionsMonitor serverOptions, + public Smtp4devServer(IServiceScopeFactory serviceScopeFactory, IOptionsMonitor serverOptions, IOptionsMonitor relayOptions, NotificationsHub notificationsHub, Func relaySmtpClientFactory, ITaskQueue taskQueue, ScriptingHost scriptingHost) { @@ -53,11 +54,11 @@ public Smtp4devServer(IServiceScopeFactory serviceScopeFactory, IOptionsMonitor< private void StartWatchingServerOptionsForChanges() { IDisposable eventHandler = null; - var obs = Observable.FromEvent(e => eventHandler = this.serverOptions.OnChange(e), e => eventHandler.Dispose()); + var obs = Observable.FromEvent(e => eventHandler = this.serverOptions.OnChange(e), e => eventHandler.Dispose()); obs.Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(OnServerOptionsChanged); } - private void OnServerOptionsChanged(ServerOptions arg1) + private void OnServerOptionsChanged(Settings.ServerOptions arg1) { if (this.smtpServer?.IsRunning == true) { @@ -75,8 +76,8 @@ private void CreateSmtpServer() { X509Certificate2 cert = CertificateHelper.GetTlsCertificate(serverOptions.CurrentValue, log); - ServerOptions serverOptionsValue = serverOptions.CurrentValue; - this.smtpServer = new Rnwood.SmtpServer.SmtpServer(new SmtpServer.ServerOptions(serverOptionsValue.AllowRemoteConnections, !serverOptionsValue.DisableIPv6, serverOptionsValue.HostName, serverOptionsValue.Port, + Settings.ServerOptions serverOptionsValue = serverOptions.CurrentValue; + this.smtpServer = new Rnwood.SmtpServer.SmtpServer(new SmtpServer.ServerOptions(serverOptionsValue.AllowRemoteConnections, !serverOptionsValue.DisableIPv6, serverOptionsValue.HostName, serverOptionsValue.Port, serverOptionsValue.AuthenticationRequired, serverOptionsValue.TlsMode == TlsMode.ImplicitTls ? cert : null, serverOptionsValue.TlsMode == TlsMode.StartTls ? cert : null )); @@ -196,14 +197,38 @@ private Task OnAuthenticationCredentialsValidationRequired(object sender, Authen var apiSession = new ApiModel.Session(session); - AuthenticationResult result = scriptingHost.ValidateCredentials(apiSession, e.Credentials); + AuthenticationResult? result = scriptingHost.ValidateCredentials(apiSession, e.Credentials); - e.AuthenticationResult = result; + if (result == null) + { + if (e.Credentials is IAuthenticationCredentialsCanValidateWithPassword val) + { + var user = serverOptions.CurrentValue.Users.FirstOrDefault(u => u.Username.Equals(val.Username, StringComparison.CurrentCultureIgnoreCase)); + if (user != null && val.ValidateResponse(user.Password)) + { + result = AuthenticationResult.Success; + this.log.Warning("SMTP auth success for user {user}", val.Username); + + } + else + { + result = AuthenticationResult.Failure; + this.log.Warning("SMTP auth failure for user {user}", val.Username); + } + } + else + { + result = AuthenticationResult.Failure; + this.log.Warning("SMTP auth failure: Cannot validate credentials of type {type}", e.Credentials.Type); + } + } + + e.AuthenticationResult = result.Value; return Task.CompletedTask; } - private readonly IOptionsMonitor serverOptions; + private readonly IOptionsMonitor serverOptions; private readonly IOptionsMonitor relayOptions; private readonly IDictionary activeSessionsToDbId = new Dictionary(); private readonly ScriptingHost scriptingHost; diff --git a/Rnwood.Smtp4dev/Service/HostingEnvironmentHelper.cs b/Rnwood.Smtp4dev/Service/HostingEnvironmentHelper.cs index 2b50be97b..87ee34a25 100644 --- a/Rnwood.Smtp4dev/Service/HostingEnvironmentHelper.cs +++ b/Rnwood.Smtp4dev/Service/HostingEnvironmentHelper.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -using Rnwood.Smtp4dev.Server; +using Rnwood.Smtp4dev.Server.Settings; namespace Rnwood.Smtp4dev.Service { diff --git a/Rnwood.Smtp4dev/Startup.cs b/Rnwood.Smtp4dev/Startup.cs index aa33dde0d..d4da18af7 100644 --- a/Rnwood.Smtp4dev/Startup.cs +++ b/Rnwood.Smtp4dev/Startup.cs @@ -24,6 +24,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.ResponseCompression; +using Rnwood.Smtp4dev.Server.Settings; namespace Rnwood.Smtp4dev { diff --git a/Rnwood.Smtp4dev/appsettings.Development.json b/Rnwood.Smtp4dev/appsettings.Development.json index 91cf574e3..660f81cdb 100644 --- a/Rnwood.Smtp4dev/appsettings.Development.json +++ b/Rnwood.Smtp4dev/appsettings.Development.json @@ -2,10 +2,16 @@ "ServerOptions": { "TlsMode": "StartTls", "Port": 25, - "BasePath" : "/", + "BasePath": "/", "AuthenticationRequired": true, "SecureConnectionRequired": true, - "CredentialsValidationExpression": "credentials.username == 'foo'" + "CredentialsValidationExpression": "", + "Users": [ + { + "Username": "rob@localhost", + "Password": "paassword" + } + ] }, "RelayOptions": { diff --git a/Rnwood.Smtp4dev/appsettings.json b/Rnwood.Smtp4dev/appsettings.json index 55f355a05..f97a537a3 100644 --- a/Rnwood.Smtp4dev/appsettings.json +++ b/Rnwood.Smtp4dev/appsettings.json @@ -67,8 +67,9 @@ //Default value: false "DisableMessageSanitisation": false, - // True if the SMTP session will require authentication + // True if SMTP and IMAP will require authentication // The client will recieve an error if a message is attempted without authentication. + // See 'Users' "AuthenticationRequired": false, // True if the SMTP session will require a secure connection. @@ -111,7 +112,14 @@ // - Rejects messages that include in the subject // message.subject.includes("19") ? 441 : null // - Rejects messages that include 19 with a 441, otherwise accepts - "MessageValidationExpression": "" + "MessageValidationExpression": "", + + "Users": [ + //{ + // "Username": "username", + // "Password": "password" + //} + ] }, "RelayOptions": { @@ -190,4 +198,6 @@ "ClientOptions": { "PageSize": 30 } + + } \ No newline at end of file diff --git a/smtpserver/Rnwood.SmtpServer/Extensions/Auth/AuthMechanisms.cs b/smtpserver/Rnwood.SmtpServer/Extensions/Auth/AuthMechanisms.cs index b25281852..2b3d92eb1 100644 --- a/smtpserver/Rnwood.SmtpServer/Extensions/Auth/AuthMechanisms.cs +++ b/smtpserver/Rnwood.SmtpServer/Extensions/Auth/AuthMechanisms.cs @@ -21,6 +21,5 @@ public static IEnumerable All() yield return new CramMd5Mechanism(); yield return new PlainMechanism(); yield return new LoginMechanism(); - yield return new AnonymousMechanism(); } } diff --git a/smtpserver/Rnwood.SmtpServer/Extensions/Auth/CramMd5AuthenticationCredentials.cs b/smtpserver/Rnwood.SmtpServer/Extensions/Auth/CramMd5AuthenticationCredentials.cs index 43fa49522..35d523803 100644 --- a/smtpserver/Rnwood.SmtpServer/Extensions/Auth/CramMd5AuthenticationCredentials.cs +++ b/smtpserver/Rnwood.SmtpServer/Extensions/Auth/CramMd5AuthenticationCredentials.cs @@ -12,7 +12,7 @@ namespace Rnwood.SmtpServer.Extensions.Auth; /// /// Defines the . /// -public class CramMd5AuthenticationCredentials : IAuthenticationCredentials +public class CramMd5AuthenticationCredentials : IAuthenticationCredentialsCanValidateWithPassword { /// /// Initializes a new instance of the class. diff --git a/smtpserver/Rnwood.SmtpServer/Extensions/Auth/IAuthenticationCredentials.cs b/smtpserver/Rnwood.SmtpServer/Extensions/Auth/IAuthenticationCredentials.cs index 5bf233c2d..a0ea0e2ae 100644 --- a/smtpserver/Rnwood.SmtpServer/Extensions/Auth/IAuthenticationCredentials.cs +++ b/smtpserver/Rnwood.SmtpServer/Extensions/Auth/IAuthenticationCredentials.cs @@ -13,4 +13,5 @@ public interface IAuthenticationCredentials /// Gets a string representing the type of this credential. /// string Type { get; } + } diff --git a/smtpserver/Rnwood.SmtpServer/Extensions/Auth/IAuthenticationCredentialsCanValidateWithPassword.cs b/smtpserver/Rnwood.SmtpServer/Extensions/Auth/IAuthenticationCredentialsCanValidateWithPassword.cs new file mode 100644 index 000000000..78ff690f0 --- /dev/null +++ b/smtpserver/Rnwood.SmtpServer/Extensions/Auth/IAuthenticationCredentialsCanValidateWithPassword.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Rnwood.SmtpServer project contributors. All rights reserved. +// Licensed under the BSD license. See LICENSE.md file in the project root for full license information. +// + +namespace Rnwood.SmtpServer.Extensions.Auth; + +public interface IAuthenticationCredentialsCanValidateWithPassword : IAuthenticationCredentials +{ + string Username { get; } + + bool ValidateResponse(string password); +} diff --git a/smtpserver/Rnwood.SmtpServer/Rnwood.SmtpServer.xml b/smtpserver/Rnwood.SmtpServer/Rnwood.SmtpServer.xml index 8dc7dee53..e6dce3fae 100644 --- a/smtpserver/Rnwood.SmtpServer/Rnwood.SmtpServer.xml +++ b/smtpserver/Rnwood.SmtpServer/Rnwood.SmtpServer.xml @@ -2390,14 +2390,14 @@ - + Initializes a new instance of the class. if set to true remote connections to the server are allowed. If IPV6 dual stack should be enabled - + Initializes a new instance of the class. @@ -2405,7 +2405,7 @@ If IPV6 dual stack should be enabled The port number. - + Initializes a new instance of the class. @@ -2414,7 +2414,7 @@ The port number. The TLS certificate to use for implicit TLS. - + Initializes a new instance of the class. @@ -2424,7 +2424,7 @@ The TLS certificate to use for implicit TLS. The TLS certificate to use for STARTTLS. - + Initializes a new instance of the class. @@ -2435,7 +2435,7 @@ The TLS certificate to use for implicit TLS. The TLS certificate to use for STARTTLS. - + Initializes a new instance of the class. diff --git a/smtpserver/Rnwood.SmtpServer/ServerOptions.cs b/smtpserver/Rnwood.SmtpServer/ServerOptions.cs index 309c01529..f9e215aff 100644 --- a/smtpserver/Rnwood.SmtpServer/ServerOptions.cs +++ b/smtpserver/Rnwood.SmtpServer/ServerOptions.cs @@ -23,7 +23,7 @@ public class ServerOptions : IServerOptions { private readonly bool allowRemoteConnections; private readonly bool enableIpV6; - + private readonly bool requireAuthentication; private readonly X509Certificate implcitTlsCertificate; private readonly X509Certificate startTlsCertificate; @@ -32,8 +32,8 @@ public class ServerOptions : IServerOptions /// /// if set to true remote connections to the server are allowed. /// If IPV6 dual stack should be enabled - public ServerOptions(bool allowRemoteConnections, bool enableIpV6) - : this(allowRemoteConnections, enableIpV6, 25, null, null) + public ServerOptions(bool allowRemoteConnections, bool enableIpV6, bool requireAuthentication) + : this(allowRemoteConnections, enableIpV6, 25, requireAuthentication, null, null) { } @@ -43,8 +43,8 @@ public ServerOptions(bool allowRemoteConnections, bool enableIpV6) /// if set to true remote connections to the server are allowed. /// If IPV6 dual stack should be enabled /// The port number. - public ServerOptions(bool allowRemoteConnections, bool enableIpV6, int portNumber) - : this(allowRemoteConnections, enableIpV6, portNumber, null, null) + public ServerOptions(bool allowRemoteConnections, bool enableIpV6, int portNumber , bool requireAuthentication) + : this(allowRemoteConnections, enableIpV6, portNumber, requireAuthentication, null, null) { } @@ -55,8 +55,8 @@ public ServerOptions(bool allowRemoteConnections, bool enableIpV6, int portNumbe /// If IPV6 dual stack should be enabled /// The port number. /// The TLS certificate to use for implicit TLS. - public ServerOptions(bool allowRemoteConnections, bool enableIpV6, int portNumber, X509Certificate implicitTlsCertificate) - : this(allowRemoteConnections, enableIpV6, portNumber, implicitTlsCertificate, null) + public ServerOptions(bool allowRemoteConnections, bool enableIpV6, int portNumber, bool requireAuthentication, X509Certificate implicitTlsCertificate) + : this(allowRemoteConnections, enableIpV6, portNumber, requireAuthentication, implicitTlsCertificate, null) { } @@ -72,9 +72,10 @@ public ServerOptions( bool allowRemoteConnections, bool enableIpV6, int portNumber, + bool requireAuthentication, X509Certificate implicitTlsCertificate, X509Certificate startTlsCertificate) - : this(allowRemoteConnections, enableIpV6, Dns.GetHostName(), portNumber, implicitTlsCertificate, startTlsCertificate) + : this(allowRemoteConnections, enableIpV6, Dns.GetHostName(), portNumber, requireAuthentication, implicitTlsCertificate, startTlsCertificate) { } @@ -92,6 +93,7 @@ public ServerOptions( bool enableIpV6, string domainName, int portNumber, + bool requireAuthentication, X509Certificate implcitTlsCertificate, X509Certificate startTlsCertificate) { @@ -101,6 +103,7 @@ public ServerOptions( this.startTlsCertificate = startTlsCertificate; this.allowRemoteConnections = allowRemoteConnections; this.enableIpV6 = enableIpV6; + this.requireAuthentication = requireAuthentication; } /// @@ -109,8 +112,8 @@ public ServerOptions( /// if set to true remote connections to the server are allowed. /// If IPV6 dual stack should be enabled /// The TLS certificate to use for implicit TLS. - public ServerOptions(bool allowRemoteConnections, bool enableIpV6, X509Certificate implcitTlsCertificate) - : this(allowRemoteConnections, enableIpV6, 587, implcitTlsCertificate, null) + public ServerOptions(bool allowRemoteConnections, bool enableIpV6, bool requireAuthentication, X509Certificate implcitTlsCertificate) + : this(allowRemoteConnections, enableIpV6, 587, requireAuthentication, implcitTlsCertificate, null) { } @@ -187,7 +190,7 @@ public virtual Task GetSSLCertificate(IConnection connection) = /// public virtual Task IsAuthMechanismEnabled(IConnection connection, IAuthMechanism authMechanism) => - Task.FromResult( + Task.FromResult(this.requireAuthentication && EnabledAuthMechanisms.Contains(authMechanism)); ///