Skip to content

Commit

Permalink
Enable ipv6 for SMTP and IMAP servers. (#1408)
Browse files Browse the repository at this point in the history
  • Loading branch information
rnwood authored Apr 19, 2024
1 parent 4c5afbc commit 0f60772
Show file tree
Hide file tree
Showing 43 changed files with 696 additions and 653 deletions.
1 change: 1 addition & 0 deletions Rnwood.Smtp4dev/ApiModel/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class Server
public bool SecureConnectionRequired { get; set; }
public string RecipientValidationExpression { get; set; }
public string MessageValidationExpression { get; set; }
public bool DisableIPv6 { get; set; }
}

}
4 changes: 3 additions & 1 deletion Rnwood.Smtp4dev/ClientApp/src/ApiClient/Server.ts
Original file line number Diff line number Diff line change
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, messageValidationExpression : string) {
secureConnectionRequired: boolean, recipientValidationExpression: string, messageValidationExpression: string, disableIPv6: string) {

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


Expand All @@ -46,4 +47,5 @@ export default class Server {
secureConnectionRequired: boolean;
recipientValidationExpression: string;
messageValidationExpression: string;
disableIPv6: string;
}
49 changes: 25 additions & 24 deletions Rnwood.Smtp4dev/ClientApp/src/components/settingsdialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,34 @@

<el-form v-if="server" :model="this" ref="form" :rules="rules" :disabled="saving">
<el-tabs tab-position="top">
<el-tab-pane label="SMTP Server">
<el-form-item label="Hostname" prop="server.hostName">
<el-tab-pane label="General">
<el-form-item label="Hostname (SMTP, IMAP)" prop="server.hostName">
<el-input v-model="server.hostName" />
</el-form-item>
<el-form-item label="Allow Remote Connections (SMTP, IMAP)" prop="server.allowRemoteConnections">
<el-switch v-model="server.allowRemoteConnections" />
</el-form-item>
<el-form-item label="Disable IPv6 (SMTP, IMAP)" prop="server.disableIPv6">
<el-switch v-model="server.disableIPv6" />
</el-form-item>

<el-form-item label="Port Number" prop="server.portNumber">
<el-input-number :min=1 :max=65535 controls-position="right" v-model="server.portNumber" />
<el-form-item label="# of Messages to Keep" prop="server.numberOfMessagesToKeep">
<el-input-number :min=1 controls-position="right" v-model="server.numberOfMessagesToKeep" />
</el-form-item>

<el-form-item label="Allow Remote Connections" prop="server.allowRemoteConnections">
<el-switch v-model="server.allowRemoteConnections" />
<el-form-item label="# of Sessions to Keep" prop="server.numberOfSessionsToKeep">
<el-input-number :min=1 controls-position="right" v-model="server.numberOfSessionsToKeep" />
</el-form-item>

<el-form-item label="Disable HTML message sanitisation on display (DANGER!)" prop="server.disableMessageSanitisation">
<el-switch v-model="server.disableMessageSanitisation" />
</el-form-item>
</el-tab-pane>
<el-tab-pane label="SMTP Server">


<el-form-item label="Port Number (0=auto assign)" prop="server.portNumber">
<el-input-number :min=0 :max=65535 controls-position="right" v-model="server.portNumber" />
</el-form-item>

<el-form-item label="TLS mode" prop="server.tlsMode">
Expand Down Expand Up @@ -51,19 +68,8 @@


<el-tab-pane label="IMAP Server">
<el-form-item label="Port Number" prop="server.imapPortNumber">
<el-input-number :min=1 :max=65535 controls-position="right" v-model="server.imapPortNumber" />
</el-form-item>
</el-tab-pane>

<el-tab-pane label="Limits">

<el-form-item label="# of Messages to Keep" prop="server.numberOfMessagesToKeep">
<el-input-number :min=1 controls-position="right" v-model="server.numberOfMessagesToKeep" />
</el-form-item>

<el-form-item label="# of Sessions to Keep" prop="server.numberOfSessionsToKeep">
<el-input-number :min=1 controls-position="right" v-model="server.numberOfSessionsToKeep" />
<el-form-item label="Port Number (0=auto assign)" prop="server.imapPortNumber">
<el-input-number :min=0 :max=65535 controls-position="right" v-model="server.imapPortNumber" />
</el-form-item>
</el-tab-pane>
<el-tab-pane label="Message Relay">
Expand Down Expand Up @@ -120,11 +126,6 @@
<el-button size="small" @click="server.relayOptions.automaticEmails.push('')">New Auto-Relay Recipient</el-button>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="User Interface">
<el-form-item label="Disable message sanitisation (DANGER!)" prop="server.disableMessageSanitisation">
<el-switch v-model="server.disableMessageSanitisation" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
</div>
Expand Down
1 change: 1 addition & 0 deletions Rnwood.Smtp4dev/CommandLineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static MapOptions<CommandLineOptions> TryParseCommandLine(IEnumerable<str
{ "baseappdatapath=","Set the base config and appData path", data => map.Add(data, x => x.BaseAppDataPath)},
{ "hostname=", "Specifies the server hostname. Used in auto-generated TLS certificate if enabled.", data => map.Add(data, x => x.ServerOptions.HostName) },
{ "allowremoteconnections", "Specifies if remote connections will be allowed to the SMTP and IMAP servers. Use -allowremoteconnections+ to enable or -allowremoteconnections- to disable", data => map.Add((data !=null).ToString(), x => x.ServerOptions.AllowRemoteConnections) },
{ "disableipv6", "If true, SMTP and IMAP servers will NOT listen using IPv6 Dual Stack", data => map.Add((data !=null).ToString(), x => x.ServerOptions.DisableIPv6)},
{ "smtpport=", "Set the port the SMTP server listens on. Specify 0 to assign automatically", data => map.Add(data, x => x.ServerOptions.Port) },
{ "db=", "Specifies the path where the database will be stored relative to APPDATA env var on Windows or XDG_CONFIG_HOME on non-Windows. Specify \"\" to use an in memory database.", data => map.Add(data, x => x.ServerOptions.Database) },
{ "messagestokeep=", "Specifies the number of messages to keep", data => map.Add(data, x=> x.ServerOptions.NumberOfMessagesToKeep) },
Expand Down
6 changes: 4 additions & 2 deletions Rnwood.Smtp4dev/Controllers/ServerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ public ApiModel.Server GetServer()
SecureConnectionRequired = serverOptions.CurrentValue.SecureConnectionRequired,
CredentialsValidationExpression = serverOptions.CurrentValue.CredentialsValidationExpression,
RecipientValidationExpression = serverOptions.CurrentValue.RecipientValidationExpression,
MessageValidationExpression = serverOptions.CurrentValue.MessageValidationExpression
MessageValidationExpression = serverOptions.CurrentValue.MessageValidationExpression,
DisableIPv6 = serverOptions.CurrentValue.DisableIPv6
};
}

Expand All @@ -79,7 +80,7 @@ public ApiModel.Server GetServer()
/// Settings can not be updated if disabled in smtp4dev settings or if the settings file is not writable.
/// <param name="serverUpdate"></param>
[HttpPost]
[SwaggerResponse(System.Net.HttpStatusCode.OK, typeof(void), Description="If settions were successfully updated and state changes initiated (not not applied synchronously and can fail).")]
[SwaggerResponse(System.Net.HttpStatusCode.OK, typeof(void), Description="If settings were successfully updated and state changes initiated (not not applied synchronously and can fail).")]
[SwaggerResponse( System.Net.HttpStatusCode.Forbidden, typeof(void), Description = "If settings are not editable")]
public ActionResult UpdateServer(ApiModel.Server serverUpdate)
{
Expand All @@ -103,6 +104,7 @@ public ActionResult UpdateServer(ApiModel.Server serverUpdate)
newSettings.CredentialsValidationExpression = serverUpdate.CredentialsValidationExpression;
newSettings.RecipientValidationExpression = serverUpdate.RecipientValidationExpression;
newSettings.MessageValidationExpression = serverUpdate.MessageValidationExpression;
newSettings.DisableIPv6 = serverUpdate.DisableIPv6;

newRelaySettings.SmtpServer = serverUpdate.RelayOptions.SmtpServer;
newRelaySettings.SmtpPort = serverUpdate.RelayOptions.SmtpPort;
Expand Down
9 changes: 0 additions & 9 deletions Rnwood.Smtp4dev/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
{
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "/",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Rnwood.Smtp4dev": {
"commandName": "Project",
"commandLineArgs": "--imapport=0 --basepath=/smtp4dev",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand Down
10 changes: 9 additions & 1 deletion Rnwood.Smtp4dev/Rnwood.Smtp4dev.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<PackageReference Include="MimeKit" Version="4.5.0" />
<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.LumiSoft.Net" Version="1.0.1" />


<PackageReference Include="Serilog" Version="3.1.1" />
Expand Down Expand Up @@ -150,6 +150,14 @@
<Touch Files="ClientApp\node_modules\.installedtimestamp" AlwaysCreate="true" />
</Target>

<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TypeScriptCompileOnSaveEnabled>False</TypeScriptCompileOnSaveEnabled>
</PropertyGroup>

<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TypeScriptCompileOnSaveEnabled>False</TypeScriptCompileOnSaveEnabled>
</PropertyGroup>

<PropertyGroup>
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
<UserSecretsId>166ca2db-ad20-40b7-95ab-1634e9dd4404</UserSecretsId>
Expand Down
3 changes: 2 additions & 1 deletion Rnwood.Smtp4dev/Server/ISmtp4devServer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Net;
using System.Threading.Tasks;
using MimeKit;
using Rnwood.Smtp4dev.DbModel;
Expand All @@ -10,7 +11,7 @@ public interface ISmtp4devServer
RelayResult TryRelayMessage(Message message, MailboxAddress[] overrideRecipients);
Exception Exception { get; }
bool IsRunning { get; }
int PortNumber { get; }
public IPEndPoint[] ListeningEndpoints { get; }
void TryStart();
void Stop();
Task DeleteSession(Guid id);
Expand Down
52 changes: 46 additions & 6 deletions Rnwood.Smtp4dev/Server/ImapServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
using Serilog;
using Microsoft.Extensions.Hosting;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Org.BouncyCastle.Utilities.Net;

namespace Rnwood.Smtp4dev.Server
{
Expand All @@ -31,7 +33,7 @@ public ImapServer(IOptionsMonitor<ServerOptions> serverOptions, IServiceScopeFac
IDisposable eventHandler = null;
var obs = Observable.FromEvent<ServerOptions>(e => eventHandler = serverOptions.OnChange(e), e => eventHandler.Dispose());
obs.Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(OnServerOptionsChanged);

}

private void OnServerOptionsChanged(ServerOptions serverOptions)
Expand All @@ -49,17 +51,44 @@ public bool IsRunning
}
}

public void TryStart()
public async void TryStart()
{
if (!serverOptions.CurrentValue.ImapPort.HasValue)
{
log.Information("IMAP server disabled");
return;
}


List<IPBindInfo> bindings = new List<IPBindInfo>();


if (serverOptions.CurrentValue.AllowRemoteConnections)
{
if (!serverOptions.CurrentValue.DisableIPv6)
{
bindings.Add(new IPBindInfo(serverOptions.CurrentValue.HostName, BindInfoProtocol.TCP, System.Net.IPAddress.IPv6Any, serverOptions.CurrentValue.ImapPort.Value));
}
else
{
bindings.Add(new IPBindInfo(serverOptions.CurrentValue.HostName, BindInfoProtocol.TCP, System.Net.IPAddress.Any, serverOptions.CurrentValue.ImapPort.Value));

}
}
else
{
bindings.Add(new IPBindInfo(serverOptions.CurrentValue.HostName, BindInfoProtocol.TCP, System.Net.IPAddress.Loopback, serverOptions.CurrentValue.ImapPort.Value));

if (!serverOptions.CurrentValue.DisableIPv6)
{
bindings.Add(new IPBindInfo(serverOptions.CurrentValue.HostName, BindInfoProtocol.TCP, System.Net.IPAddress.IPv6Loopback, serverOptions.CurrentValue.ImapPort.Value));
}
}

imapServer = new IMAP_Server()
{
Bindings = new[] { new IPBindInfo(Dns.GetHostName(), BindInfoProtocol.TCP, serverOptions.CurrentValue.AllowRemoteConnections ? IPAddress.Any : IPAddress.Loopback, serverOptions.CurrentValue.ImapPort.Value) },

Bindings = bindings.ToArray(),
GreetingText = "smtp4dev"
};
imapServer.SessionCreated += (o, ea) => new SessionHandler(ea.Session, this.serviceScopeFactory);
Expand All @@ -86,7 +115,8 @@ public void TryStart()
if (index == 1)
{
log.Warning("The IMAP server failed to start: {Exception}" + errorTask.Result.Exception.ToString());
} else if (index == 2)
}
else if (index == 2)
{
log.Warning("The IMAP server failed to start: Timeout");

Expand All @@ -98,8 +128,18 @@ public void TryStart()
}
else
{
int port = ((IPEndPoint)imapServer.ListeningPoints[0].Socket.LocalEndPoint).Port;
log.Information("IMAP Server is listening on port {port}", port);
while(imapServer.ListeningPoints.Length < imapServer.Bindings.Length)
{
await Task.Delay(100);
}

foreach(var lp in imapServer.ListeningPoints)
{
var ep = ((IPEndPoint)lp.Socket.LocalEndPoint);
int port = ep.Port;
log.Information("IMAP Server is listening on port {port} ({address})", port, ep.Address);
}

}
}

Expand Down
1 change: 1 addition & 0 deletions Rnwood.Smtp4dev/Server/ServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ public class ServerOptions
public string RecipientValidationExpression { get; set; }

public string MessageValidationExpression { get; set; }
public bool DisableIPv6 { get; set; } = false;
}
}
21 changes: 13 additions & 8 deletions Rnwood.Smtp4dev/Server/Smtp4devServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using SmtpResponse = Rnwood.SmtpServer.SmtpResponse;
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Net;

namespace Rnwood.Smtp4dev.Server
{
Expand Down Expand Up @@ -75,19 +76,19 @@ private void CreateSmtpServer()
X509Certificate2 cert = CertificateHelper.GetTlsCertificate(serverOptions.CurrentValue, log);

ServerOptions serverOptionsValue = serverOptions.CurrentValue;
this.smtpServer = new DefaultServer(serverOptionsValue.AllowRemoteConnections, serverOptionsValue.HostName, serverOptionsValue.Port,
this.smtpServer = new Rnwood.SmtpServer.SmtpServer(new SmtpServer.ServerOptions(serverOptionsValue.AllowRemoteConnections, !serverOptionsValue.DisableIPv6, serverOptionsValue.HostName, serverOptionsValue.Port,
serverOptionsValue.TlsMode == TlsMode.ImplicitTls ? cert : null,
serverOptionsValue.TlsMode == TlsMode.StartTls ? cert : null
);
));
this.smtpServer.MessageCompletedEventHandler += OnMessageCompleted;
this.smtpServer.MessageReceivedEventHandler += OnMessageReceived;
this.smtpServer.SessionCompletedEventHandler += OnSessionCompleted;
this.smtpServer.SessionStartedHandler += OnSessionStarted;
this.smtpServer.AuthenticationCredentialsValidationRequiredEventHandler += OnAuthenticationCredentialsValidationRequired;
this.smtpServer.IsRunningChanged += OnIsRunningChanged;
((DefaultServerBehaviour)this.smtpServer.Behaviour).MessageStartEventHandler += OnMessageStart;
((SmtpServer.ServerOptions)this.smtpServer.Options).MessageStartEventHandler += OnMessageStart;

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

private Task OnMessageRecipientAddingEventHandler(object sender, RecipientAddingEventArgs e)
Expand Down Expand Up @@ -405,7 +406,7 @@ private void TrimSessions(Smtp4devDbContext dbContext)


private readonly ITaskQueue taskQueue;
private DefaultServer smtpServer;
private Rnwood.SmtpServer.SmtpServer smtpServer;
private readonly Func<RelayOptions, SmtpClient> relaySmtpClientFactory;
private readonly NotificationsHub notificationsHub;
private readonly IServiceScopeFactory serviceScopeFactory;
Expand All @@ -414,7 +415,7 @@ private void TrimSessions(Smtp4devDbContext dbContext)

public bool IsRunning => this.smtpServer.IsRunning;

public int PortNumber => this.smtpServer.PortNumber;
public IPEndPoint[] ListeningEndpoints => this.smtpServer.ListeningEndpoints;

public void TryStart()
{
Expand All @@ -425,8 +426,12 @@ public void TryStart()
CreateSmtpServer();
smtpServer.Start();

log.Information("SMTP Server is listening on port {smtpPortNumber}.",
smtpServer.PortNumber);
foreach (var l in smtpServer.ListeningEndpoints)
{
log.Information("SMTP Server is listening on port {smtpPortNumber} ({address}).",
l.Port, l.Address);
}

log.Information("Keeping last {messagesToKeep} messages and {sessionsToKeep} sessions.",
serverOptions.CurrentValue.NumberOfMessagesToKeep, serverOptions.CurrentValue.NumberOfSessionsToKeep);
}
Expand Down
Loading

0 comments on commit 0f60772

Please sign in to comment.