Skip to content

Commit

Permalink
Feature(settings):Add credentials and recipient validation settings a…
Browse files Browse the repository at this point in the history
…nd add more existing settings to the UI (#1349)
  • Loading branch information
rnwood committed Mar 16, 2024
1 parent 5ea400f commit 4155c6d
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 39 deletions.
5 changes: 5 additions & 0 deletions Rnwood.Smtp4dev/ApiModel/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public class Server
public int? ImapPortNumber { get; set; }

public bool DisableMessageSanitisation { get; set; }
public string TlsMode { get; set; }
public bool AuthenticationRequired { get; set; }
public string CredentialsValidationExpression { get; set; }
public bool SecureConnectionRequired { get; set; }
public string RecipientValidationExpression { get; set; }
}

}
16 changes: 14 additions & 2 deletions Rnwood.Smtp4dev/ClientApp/src/ApiClient/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import ServerRelayOptions from './ServerRelayOptions';
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) {
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) {

this.isRunning = isRunning;
this.exception = exception;
Expand All @@ -16,7 +18,12 @@ export default class Server {
this.imapPortNumber = imapPortNumber;
this.settingsAreEditable = settingsAreEditable;
this.disableMessageSanitisation = disableMessageSanitisation;
this.automaticRelayExpression = automaticRelayExpression
this.automaticRelayExpression = automaticRelayExpression;
this.tlsMode = tlsMode;
this.credentialsValidationExpression = credentialsValidationExpression;
this.authenticationRequired = authenticationRequired;
this.secureConnectionRequired = secureConnectionRequired;
this.recipientValidationExpression = recipientValidationExpression;
}


Expand All @@ -32,4 +39,9 @@ export default class Server {
settingsAreEditable: boolean;
disableMessageSanitisation: boolean;
automaticRelayExpression: string;
tlsMode: string;
credentialsValidationExpression: string;
authenticationRequired: boolean;
secureConnectionRequired: boolean;
recipientValidationExpression: string;
}
32 changes: 28 additions & 4 deletions Rnwood.Smtp4dev/ClientApp/src/components/settingsdialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@
<el-form-item label="Allow Remote Connections" prop="server.allowRemoteConnections">
<el-switch v-model="server.allowRemoteConnections" />
</el-form-item>

<el-form-item label="TLS mode" prop="server.tlsMode">
<el-select v-model="server.tlsMode" style="width: 100%;">
<el-option key="None" label="None" value="None"></el-option>
<el-option key="StartTls" label="STARTTLS (client requests TLS after session starts)" value="StartTls"></el-option>
<el-option key="ImplicitTls" label="Implicit TLS (TLS immediately)" value="ImplicitTls"></el-option>
</el-select>
</el-form-item>

<el-form-item label="Require Secure Connection" prop="server.secureConnectionRequired">
<el-switch v-model="server.secureConnectionRequired" />
</el-form-item>

<el-form-item label="Require Authentication" prop="server.authenticationRequired">
<el-switch v-model="server.authenticationRequired" />
</el-form-item>

<el-form-item label="Credentials validation expression (see comments in appsettings.json)" prop="server.credentialsValidationExpression">
<el-input v-model="server.credentialsValidationExpression" />
</el-form-item>

<el-form-item label="Recipient validation expression (see comments in appsettings.json)" prop="server.recipientValidationExpression">
<el-input v-model="server.recipientValidationExpression" />
</el-form-item>
</el-tab-pane>


Expand Down Expand Up @@ -51,13 +75,13 @@
<el-input-number :min=1 :max=65535 controls-position="right" v-model="server.relayOptions.smtpPort" />
</el-form-item>

<el-form-item label="Tls mode" prop="server.relayOptions.tlsMode" v-show="isRelayEnabled">
<el-form-item label="TLS mode" prop="server.relayOptions.tlsMode" v-show="isRelayEnabled">
<el-select v-model="server.relayOptions.tlsMode">
<el-option key="None" label="None" value="None"></el-option>
<el-option key="Auto" label="Auto" value="Auto"></el-option>
<el-option key="SslOnConnect" label="SslOnConnect" value="SslOnConnect"></el-option>
<el-option key="StartTls" label="StartTls" value="StartTls"></el-option>
<el-option key="StartTlsWhenAvailable" label="StartTlsWhenAvailable" value="StartTlsWhenAvailable"></el-option>
<el-option key="SslOnConnect" label="TLS on connect" value="SslOnConnect"></el-option>
<el-option key="StartTls" label="STARTTLS" value="StartTls"></el-option>
<el-option key="StartTlsWhenAvailable" label="STARTTLS if available" value="StartTlsWhenAvailable"></el-option>
</el-select>
</el-form-item>

Expand Down
6 changes: 3 additions & 3 deletions Rnwood.Smtp4dev/CommandLineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public static MapOptions<CommandLineOptions> TryParseCommandLine(IEnumerable<str
{ "locksettings", "Locks settings from being changed by user via web interface", data => map.Add((data !=null).ToString(), x=> x.ServerOptions.LockSettings) },
{ "installpath=", "Sets path to folder containing wwwroot and other files", data => 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)}


{ "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) },
{ "secureconnectionrequired", "Requires that SMTP clients use SSL/TLS", data => map.Add((data !=null).ToString(), x=> x.ServerOptions.SecureConnectionRequired) }
};

if (!isDesktopApp)
Expand Down
13 changes: 11 additions & 2 deletions Rnwood.Smtp4dev/Controllers/ServerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ public ApiModel.Server GetServer()
AutomaticRelayExpression = relayOptions.CurrentValue.AutomaticRelayExpression
},
SettingsAreEditable = hostingEnvironmentHelper.SettingsAreEditable,
DisableMessageSanitisation = serverOptions.CurrentValue.DisableMessageSanitisation

DisableMessageSanitisation = serverOptions.CurrentValue.DisableMessageSanitisation,
TlsMode = serverOptions.CurrentValue.TlsMode.ToString(),
AuthenticationRequired = serverOptions.CurrentValue.AuthenticationRequired,
SecureConnectionRequired = serverOptions.CurrentValue.SecureConnectionRequired,
CredentialsValidationExpression = serverOptions.CurrentValue.CredentialsValidationExpression,
RecipientValidationExpression = serverOptions.CurrentValue.RecipientValidationExpression
};
}

Expand All @@ -79,6 +83,11 @@ public ActionResult UpdateServer(ApiModel.Server serverUpdate)
newSettings.NumberOfSessionsToKeep = serverUpdate.NumberOfSessionsToKeep;
newSettings.ImapPort = serverUpdate.ImapPortNumber;
newSettings.DisableMessageSanitisation = serverUpdate.DisableMessageSanitisation;
newSettings.TlsMode = Enum.Parse<TlsMode>(serverUpdate.TlsMode);
newSettings.AuthenticationRequired = serverUpdate.AuthenticationRequired;
newSettings.SecureConnectionRequired = serverUpdate.SecureConnectionRequired;
newSettings.CredentialsValidationExpression = serverUpdate.CredentialsValidationExpression;
newSettings.RecipientValidationExpression = serverUpdate.RecipientValidationExpression;

newRelaySettings.SmtpServer = serverUpdate.RelayOptions.SmtpServer;
newRelaySettings.SmtpPort = serverUpdate.RelayOptions.SmtpPort;
Expand Down
2 changes: 1 addition & 1 deletion Rnwood.Smtp4dev/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private static IHost BuildWebHost(string[] args, CommandLineOptions cmdLineOptio
IConfigurationBuilder cb = configBuilder
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
if (!cmdLineOptions.NoUserSettings)
{
cb = cb.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
Expand Down
2 changes: 1 addition & 1 deletion Rnwood.Smtp4dev/Rnwood.Smtp4dev.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0837" />
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0843" />
</ItemGroup>

<ItemGroup>
Expand Down
119 changes: 105 additions & 14 deletions Rnwood.Smtp4dev/Server/ScriptingHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
using Jint.Native;
using Jint.Runtime;
using Microsoft.Extensions.Options;
using Rnwood.Smtp4dev.DbModel;
using Rnwood.SmtpServer;
using Rnwood.SmtpServer.Extensions.Auth;
using Serilog;

namespace Rnwood.Smtp4dev.Server;
Expand All @@ -19,39 +21,56 @@ internal class ScriptingHost



private IOptionsMonitor<RelayOptions> _relayOptions;
private IOptionsMonitor<RelayOptions> relayOptions;
private IOptionsMonitor<ServerOptions> serverOptions;

public ScriptingHost(IOptionsMonitor<RelayOptions> relayOptions)
public ScriptingHost(IOptionsMonitor<RelayOptions> relayOptions, IOptionsMonitor<ServerOptions> serverOptions)
{
_relayOptions = relayOptions;
_relayOptions.OnChange(o => ParseScripts(o));
ParseScripts(relayOptions.CurrentValue);
this.relayOptions = relayOptions;
this.serverOptions = serverOptions;
this.relayOptions.OnChange(_ => ParseScripts(relayOptions.CurrentValue, serverOptions.CurrentValue));
this.serverOptions.OnChange(_ => ParseScripts(relayOptions.CurrentValue, serverOptions.CurrentValue));
ParseScripts(relayOptions.CurrentValue, serverOptions.CurrentValue);
}

private void ParseScripts(RelayOptions relayOptionsCurrentValue)
private void ParseScript(string type, string expression, ref Script script, ref string source)
{
if (shouldRelaySource != relayOptionsCurrentValue.AutomaticRelayExpression)
if (source != expression)
{
var autoRelayExpression = relayOptionsCurrentValue.AutomaticRelayExpression ?? "";
log.Information("Parsing AutomaticRelayExpression {autoRelayExpression}", autoRelayExpression);
expression = expression ?? "";
log.Information("Parsing {type} {expression}", type, expression);

if (string.IsNullOrWhiteSpace(autoRelayExpression))
if (string.IsNullOrWhiteSpace(expression))
{
shouldRelayScript = null;
script = null;
}
else
{
var parser = new JavaScriptParser();
shouldRelayScript = parser.ParseScript(autoRelayExpression);
script = parser.ParseScript(expression);
}

shouldRelaySource = autoRelayExpression;
source = expression;
}
}

private void ParseScripts(RelayOptions relayOptionsCurrentValue, ServerOptions serverOptionsCurrentValue)
{
ParseScript("AutomaticRelayExpression", relayOptionsCurrentValue.AutomaticRelayExpression, ref shouldRelayScript, ref shouldRelaySource);
ParseScript("CredentialsValidationExpression", serverOptionsCurrentValue.CredentialsValidationExpression, ref credValidationScript,
ref credValidationSource);
ParseScript("RecipientValidationExpression", serverOptionsCurrentValue.RecipientValidationExpression, ref recipValidationScript,
ref recipValidationSource);
}

private string shouldRelaySource;
private Script shouldRelayScript;

private string credValidationSource;
private Script credValidationScript;
private string recipValidationSource;
private Script recipValidationScript;


public IReadOnlyCollection<string> GetAutoRelayRecipients(ApiModel.Message message, string recipient, ApiModel.Session session)
{
if (shouldRelayScript == null)
Expand Down Expand Up @@ -104,4 +123,76 @@ public IReadOnlyCollection<string> GetAutoRelayRecipients(ApiModel.Message messa
}

}

public AuthenticationResult ValidateCredentials(Session session, IAuthenticationCredentials credentials)
{
if (credValidationScript == null)
{
return AuthenticationResult.Success;
}

Engine jsEngine = new Engine();

jsEngine.SetValue("credentials", credentials);
jsEngine.SetValue("session", session);

try
{
JsValue result = jsEngine.Evaluate(credValidationScript);

bool success = result.AsBoolean();

log.Information("CredentialValidationExpression: (credentials: {credentials}, session: {session.Id}) => {result} => {success}", credentials,
session.Id, result, success);

return success ? AuthenticationResult.Success : AuthenticationResult.Failure;

}
catch (JavaScriptException ex)
{
log.Error("Error executing CredentialValidationExpression : {error}", ex.Error);
return AuthenticationResult.TemporaryFailure;
}
catch (Exception ex)
{
log.Error("Error executing CredentialValidationExpression : {error}", ex.ToString());
return AuthenticationResult.TemporaryFailure;
}
}

public bool ValidateRecipient(Session session, string recipient)
{
if (recipValidationScript == null)
{
return true;
}

Engine jsEngine = new Engine();

jsEngine.SetValue("recipient", recipient);
jsEngine.SetValue("session", session);

try
{
JsValue result = jsEngine.Evaluate(recipValidationScript);

bool success = result.AsBoolean();

log.Information("RecipientValidationExpression: (recipient: {recipient}, session: {session.Id}) => {result} => {success}", recipient,
session.Id, result, success);

return success;

}
catch (JavaScriptException ex)
{
log.Error("Error executing RecipientValidationExpression : {error}", ex.Error);
return false;
}
catch (Exception ex)
{
log.Error("Error executing RecipientValidationExpression : {error}", ex.ToString());
return false;
}
}
}
5 changes: 5 additions & 0 deletions Rnwood.Smtp4dev/Server/ServerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using Esprima.Ast;

namespace Rnwood.Smtp4dev.Server
{
Expand Down Expand Up @@ -30,5 +31,9 @@ public class ServerOptions
public bool LockSettings { get; set; } = false;

public bool DisableMessageSanitisation { get; set; } = false;
public string CredentialsValidationExpression { get; set; }
public bool AuthenticationRequired { get; set; } = false;
public bool SecureConnectionRequired { get; set; } = false;
public string RecipientValidationExpression { get; set; }
}
}
43 changes: 42 additions & 1 deletion Rnwood.Smtp4dev/Server/Smtp4devServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Jint.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Rnwood.Smtp4dev.Data;
using Rnwood.SmtpServer.Extensions.Auth;
using Serilog;
using SmtpResponse = Rnwood.SmtpServer.SmtpResponse;

Expand Down Expand Up @@ -81,6 +82,39 @@ private void CreateSmtpServer()
this.smtpServer.SessionStartedHandler += OnSessionStarted;
this.smtpServer.AuthenticationCredentialsValidationRequiredEventHandler += OnAuthenticationCredentialsValidationRequired;
this.smtpServer.IsRunningChanged += OnIsRunningChanged;
((DefaultServerBehaviour)this.smtpServer.Behaviour).MessageStartEventHandler += OnMessageStart;

((DefaultServerBehaviour)this.smtpServer.Behaviour).MessageRecipientAddingEventHandler += OnMessageRecipientAddingEventHandler;
}

private Task OnMessageRecipientAddingEventHandler(object sender, RecipientAddingEventArgs e)
{
var sessionId = activeSessionsToDbId[e.Message.Session];
using var scope = serviceScopeFactory.CreateScope();
Smtp4devDbContext dbContext = scope.ServiceProvider.GetService<Smtp4devDbContext>();
var session = dbContext.Sessions.Single(s => s.Id == sessionId);

if (!this.scriptingHost.ValidateRecipient(session, e.Recipient))
{
throw new SmtpServerException(new SmtpResponse(StandardSmtpResponseCode.RecipientRejected, "Recipient rejected"));
}

return Task.CompletedTask;
}

private Task OnMessageStart(object sender, MessageStartEventArgs e)
{
if (this.serverOptions.CurrentValue.SecureConnectionRequired && !e.Session.SecureConnection)
{
throw new SmtpServerException(new SmtpResponse(451, "Secure connection required"));
}

if (this.serverOptions.CurrentValue.AuthenticationRequired && !e.Session.Authenticated)
{
throw new SmtpServerException(new SmtpResponse(StandardSmtpResponseCode.AuthenticationRequired, "Authentication is required"));
}

return Task.CompletedTask;
}

private void OnIsRunningChanged(object sender, EventArgs e)
Expand Down Expand Up @@ -130,7 +164,14 @@ private void QueueCleanup()

private Task OnAuthenticationCredentialsValidationRequired(object sender, AuthenticationCredentialsValidationEventArgs e)
{
e.AuthenticationResult = AuthenticationResult.Success;
var sessionId = activeSessionsToDbId[e.Session];
using var scope = serviceScopeFactory.CreateScope();
Smtp4devDbContext dbContext = scope.ServiceProvider.GetService<Smtp4devDbContext>();
var session = dbContext.Sessions.Single(s => s.Id == sessionId);

AuthenticationResult result = scriptingHost.ValidateCredentials(session, e.Credentials);

e.AuthenticationResult = result;
return Task.CompletedTask;
}

Expand Down

0 comments on commit 4155c6d

Please sign in to comment.