Skip to content

Commit

Permalink
Feat(Rules): Allow message validation expression #165 (#1377)
Browse files Browse the repository at this point in the history
  • Loading branch information
rnwood committed Apr 14, 2024
1 parent 0337655 commit bdb5bd4
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 50 deletions.
1 change: 1 addition & 0 deletions Rnwood.Smtp4dev/ApiModel/Server.cs
Expand Up @@ -32,6 +32,7 @@ public class Server
public string CredentialsValidationExpression { get; set; }
public bool SecureConnectionRequired { get; set; }
public string RecipientValidationExpression { get; set; }
public string MessageValidationExpression { get; set; }
}

}
4 changes: 3 additions & 1 deletion Rnwood.Smtp4dev/ApiModel/Session.cs
Expand Up @@ -12,13 +12,15 @@ public Session(DbModel.Session dbSession)
{
this.Id = dbSession.Id;
this.Error = dbSession.SessionError;
this.ErrorType = dbSession.SessionErrorType.ToString();
this.ErrorType = dbSession.SessionErrorType?.ToString();
this.StartDate = dbSession.StartDate;
}


public Guid Id { get; private set; }

public string ErrorType { get; private set; }
public DateTime StartDate { get; }
public string Error { get; private set; }


Expand Down
4 changes: 3 additions & 1 deletion Rnwood.Smtp4dev/ClientApp/src/ApiClient/Server.ts
Expand Up @@ -5,7 +5,7 @@ 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) {
secureConnectionRequired: boolean, recipientValidationExpression: string, messageValidationExpression : string) {

this.isRunning = isRunning;
this.exception = exception;
Expand All @@ -24,6 +24,7 @@ export default class Server {
this.authenticationRequired = authenticationRequired;
this.secureConnectionRequired = secureConnectionRequired;
this.recipientValidationExpression = recipientValidationExpression;
this.messageValidationExpression = messageValidationExpression;
}


Expand All @@ -44,4 +45,5 @@ export default class Server {
authenticationRequired: boolean;
secureConnectionRequired: boolean;
recipientValidationExpression: string;
messageValidationExpression: string;
}
4 changes: 4 additions & 0 deletions Rnwood.Smtp4dev/ClientApp/src/components/settingsdialog.vue
Expand Up @@ -43,6 +43,10 @@
<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-form-item label="Message validation expression (see comments in appsettings.json)" prop="server.messageValidationExpression">
<el-input v-model="server.messageValidationExpression" />
</el-form-item>
</el-tab-pane>


Expand Down
4 changes: 3 additions & 1 deletion Rnwood.Smtp4dev/Controllers/ServerController.cs
Expand Up @@ -68,7 +68,8 @@ public ApiModel.Server GetServer()
AuthenticationRequired = serverOptions.CurrentValue.AuthenticationRequired,
SecureConnectionRequired = serverOptions.CurrentValue.SecureConnectionRequired,
CredentialsValidationExpression = serverOptions.CurrentValue.CredentialsValidationExpression,
RecipientValidationExpression = serverOptions.CurrentValue.RecipientValidationExpression
RecipientValidationExpression = serverOptions.CurrentValue.RecipientValidationExpression,
MessageValidationExpression = serverOptions.CurrentValue.MessageValidationExpression
};
}

Expand Down Expand Up @@ -101,6 +102,7 @@ public ActionResult UpdateServer(ApiModel.Server serverUpdate)
newSettings.SecureConnectionRequired = serverUpdate.SecureConnectionRequired;
newSettings.CredentialsValidationExpression = serverUpdate.CredentialsValidationExpression;
newSettings.RecipientValidationExpression = serverUpdate.RecipientValidationExpression;
newSettings.MessageValidationExpression = serverUpdate.MessageValidationExpression;

newRelaySettings.SmtpServer = serverUpdate.RelayOptions.SmtpServer;
newRelaySettings.SmtpPort = serverUpdate.RelayOptions.SmtpPort;
Expand Down
33 changes: 12 additions & 21 deletions Rnwood.Smtp4dev/Rnwood.Smtp4dev.csproj
Expand Up @@ -32,6 +32,8 @@
<PackageReference Include="Jint" Version="3.0.1" />
<PackageReference Include="MailKit" Version="4.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.WindowsServices" Version="8.0.4" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand All @@ -44,6 +46,7 @@
<PackageReference Include="Mono.Options" Version="6.12.0.148" />
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7" />
<PackageReference Include="Rnwood.LumiSoft.Net" Version="1.0.0" />
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" />


<PackageReference Include="Serilog" Version="3.1.1" />
Expand All @@ -54,15 +57,10 @@
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.3.10" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
<PackageReference Include="VueCliMiddleware" Version="6.0.0" />
</ItemGroup>

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

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools"
Version="2.1.0-preview1-final" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.1.0-preview1-final" />
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -143,11 +141,9 @@
<NpmInputs Include="ClientApp\package.json" />
</ItemGroup>

<Target Name="NpmInstall" AfterTargets="Build" Inputs="@(NpmInputs)"
Outputs="ClientApp\node_modules\.installedtimestamp" Condition="'$(SkipClientApp)'!='true'">
<Target Name="NpmInstall" AfterTargets="Build" Inputs="@(NpmInputs)" Outputs="ClientApp\node_modules\.installedtimestamp" Condition="'$(SkipClientApp)'!='true'">
<Message Importance="high" Text="Performing npm install..." />
<Exec Command="npm install --no-progress" WorkingDirectory="ClientApp"
CustomErrorRegularExpression="^npm ERR!.*" />
<Exec Command="npm install --no-progress" WorkingDirectory="ClientApp" CustomErrorRegularExpression="^npm ERR!.*" />
<Touch Files="ClientApp\node_modules\.installedtimestamp" AlwaysCreate="true" />
</Target>

Expand Down Expand Up @@ -184,22 +180,18 @@
<TypeScriptCompile Include="ClientApp\src\ApiClient\ServerRelayOptions.ts" />
</ItemGroup>

<Target Name="DebugRunNpm" AfterTargets="Build" Inputs="@(ClientApp)"
Outputs="wwwroot\.buildtimestamp" Condition="'$(SkipClientApp)'!='true'">
<Target Name="DebugRunNpm" AfterTargets="Build" Inputs="@(ClientApp)" Outputs="wwwroot\.buildtimestamp" Condition="'$(SkipClientApp)'!='true'">

<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'"
Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />

<Message Importance="high" Text="Performing NPM build..." />

<Exec Condition=" '$(Configuration)' != 'Release' "
Command="npm --prefix ClientApp run-script build:dev" CustomErrorRegularExpression="^ERROR in" />
<Exec Condition=" '$(Configuration)' == 'Release' "
Command="npm --prefix ClientApp run-script build" CustomErrorRegularExpression="^ERROR in" />
<Exec Condition=" '$(Configuration)' != 'Release' " Command="npm --prefix ClientApp run-script build:dev" CustomErrorRegularExpression="^ERROR in" />
<Exec Condition=" '$(Configuration)' == 'Release' " Command="npm --prefix ClientApp run-script build" CustomErrorRegularExpression="^ERROR in" />

<Touch Files="wwwroot\.buildtimestamp" AlwaysCreate="true" />
</Target>
Expand All @@ -208,8 +200,7 @@
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="wwwroot\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')"
Exclude="@(ResolvedFileToPublish)">
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
Expand Down
93 changes: 78 additions & 15 deletions Rnwood.Smtp4dev/Server/ScriptingHost.cs
Expand Up @@ -7,6 +7,7 @@
using Jint;
using Jint.Native;
using Jint.Runtime;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Options;
using Rnwood.Smtp4dev.DbModel;
using Rnwood.SmtpServer;
Expand All @@ -19,7 +20,7 @@ internal class ScriptingHost
{
private readonly ILogger log = Log.ForContext<ScriptingHost>();



private IOptionsMonitor<RelayOptions> relayOptions;
private IOptionsMonitor<ServerOptions> serverOptions;
Expand Down Expand Up @@ -61,6 +62,8 @@ private void ParseScripts(RelayOptions relayOptionsCurrentValue, ServerOptions s
ref credValidationSource);
ParseScript("RecipientValidationExpression", serverOptionsCurrentValue.RecipientValidationExpression, ref recipValidationScript,
ref recipValidationSource);
ParseScript("MessageValidationExpression", serverOptionsCurrentValue.MessageValidationExpression, ref messageValidationScript,
ref messageValidationSource);
}

private string shouldRelaySource;
Expand All @@ -69,17 +72,21 @@ private void ParseScripts(RelayOptions relayOptionsCurrentValue, ServerOptions s
private Script credValidationScript;
private string recipValidationSource;
private Script recipValidationScript;



private string messageValidationSource;
private Script messageValidationScript;

public bool HasValidateMessageExpression { get => this.messageValidationScript != null; }

public IReadOnlyCollection<string> GetAutoRelayRecipients(ApiModel.Message message, string recipient, ApiModel.Session session)
{
if (shouldRelayScript == null)
{
return Array.Empty<string>();
}

Engine jsEngine = new Engine();

jsEngine.SetValue("recipient", recipient);
jsEngine.SetValue("message", message);
jsEngine.SetValue("session", session);
Expand All @@ -91,7 +98,7 @@ public IReadOnlyCollection<string> GetAutoRelayRecipients(ApiModel.Message messa
List<string> recpients = new List<string>();
if (result.IsNull())
{

}
else if (result.IsString())
{
Expand Down Expand Up @@ -128,15 +135,15 @@ public IReadOnlyCollection<string> GetAutoRelayRecipients(ApiModel.Message messa

}

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

Engine jsEngine = new Engine();

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

Expand All @@ -145,7 +152,7 @@ public AuthenticationResult ValidateCredentials(Session session, IAuthentication
JsValue result = jsEngine.Evaluate(credValidationScript);

bool success = result.AsBoolean();

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

Expand All @@ -163,16 +170,16 @@ public AuthenticationResult ValidateCredentials(Session session, IAuthentication
return AuthenticationResult.TemporaryFailure;
}
}
public bool ValidateRecipient(Session session, string recipient)

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

Engine jsEngine = new Engine();

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

Expand All @@ -181,7 +188,7 @@ public bool ValidateRecipient(Session session, string recipient)
JsValue result = jsEngine.Evaluate(recipValidationScript);

bool success = result.AsBoolean();

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

Expand All @@ -199,4 +206,60 @@ public bool ValidateRecipient(Session session, string recipient)
return false;
}
}

internal SmtpResponse ValidateMessage(ApiModel.Message message, ApiModel.Session session)
{
if (messageValidationScript == null)
{
return null;
}

Engine jsEngine = new Engine();

jsEngine.SetValue("message", message);
jsEngine.SetValue("session", session);
jsEngine.SetValue("error", (Action<int?, string>)((code, message) => throw new SmtpServerException(new SmtpResponse(code ?? (int)StandardSmtpResponseCode.TransactionFailed, message ?? ""))));

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

SmtpResponse response;

if (result.IsNull() || result.IsUndefined())
{
response = null;
}
else if (result.IsNumber())
{
response = new SmtpResponse((int)result.AsNumber(), "Message rejected by MessageValidationExpression");
} else if (result.IsString()) {
response = new SmtpResponse(StandardSmtpResponseCode.TransactionFailed, result.AsString());
}
else
{
response = result.AsBoolean() ? null : new SmtpResponse(StandardSmtpResponseCode.TransactionFailed, "Message rejected by MessageValidationExpression");
}

log.Information("MessageValidationExpression: (message: {message}, session: {session.Id}) => {result} => {success}", message,
session.Id, result, response?.Code.ToString() ?? "Success");

return response;

}
catch (SmtpServerException ex)
{
return ex.SmtpResponse;
}
catch (JavaScriptException ex)
{
log.Error("Error executing MessageValidationExpression : {error}", ex.Error);
return new SmtpResponse(StandardSmtpResponseCode.TransactionFailed, "MessageValidationExpression failed");
}
catch (Exception ex)
{
log.Error("Error executing MessageValidationExpression : {error}", ex.ToString());
return new SmtpResponse(StandardSmtpResponseCode.TransactionFailed, "MessageValidationExpression failed");
}
}
}
2 changes: 2 additions & 0 deletions Rnwood.Smtp4dev/Server/ServerOptions.cs
Expand Up @@ -35,5 +35,7 @@ public class ServerOptions
public bool AuthenticationRequired { get; set; } = false;
public bool SecureConnectionRequired { get; set; } = false;
public string RecipientValidationExpression { get; set; }

public string MessageValidationExpression { get; set; }
}
}

0 comments on commit bdb5bd4

Please sign in to comment.