.net update, implemented base session, added test client

This commit is contained in:
Kevin Kai Berthold 2025-12-03 18:05:41 +01:00
parent e0b732267b
commit 7b147569e0
11 changed files with 371 additions and 41 deletions

View file

@ -0,0 +1,31 @@
using Microsoft.Extensions.Logging;
namespace SmtpRelay.Graph;
public partial class GraphSmtpSession(GraphSender graphSender, ILogger<GraphSmtpSession> logger) : SmtpSessionBase(logger)
{
private readonly GraphSender _graphSender = graphSender;
protected override async Task OnProcessDataAsync(ReadOnlyMemory<byte> dataBytes, CancellationToken cancellationToken)
{
// save to spool dir (optional)
var spoolFile = await SpoolToFileAsync(dataBytes, cancellationToken);
// sent via graph api
await _graphSender.SendAsync(spoolFile, dataBytes, cancellationToken);
}
private async Task<FileInfo> SpoolToFileAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
{
Directory.CreateDirectory(Common.SpoolDir.FullName);
var path = Path.Combine(Common.SpoolDir.FullName, $"{Guid.NewGuid()}.eml");
var spoolFile = new FileInfo(path);
await File.WriteAllBytesAsync(spoolFile.FullName, data, cancellationToken);
_logger.LogInformation("Spooled: {Path}", spoolFile.FullName);
return spoolFile;
}
}

View file

@ -136,8 +136,8 @@ internal partial class Program
services.AddSingleton(graphConfig);
// register smtp instances
services.AddSingleton<SmtpServer>();
services.AddScoped<SmtpSession>();
services.AddSingleton<SmtpServer<GraphSmtpSession>>();
services.AddScoped<GraphSmtpSession>();
// register smtp service
services.AddHostedService<Service>();

View file

@ -1,11 +1,12 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SmtpRelay.Graph;
namespace SmtpRelay;
internal class Service(SmtpServer server, SmtpConfig config, ILogger<Service> logger) : BackgroundService
internal class Service(SmtpServer<GraphSmtpSession> server, SmtpConfig config, ILogger<Service> logger) : BackgroundService
{
private readonly SmtpServer _server = server;
private readonly SmtpServer<GraphSmtpSession> _server = server;
private readonly SmtpConfig _config = config;
private readonly ILogger<Service> _logger = logger;

View file

@ -5,16 +5,17 @@ using System.Net.Sockets;
namespace SmtpRelay;
public class SmtpServer(IServiceScopeFactory scopeFactory, ILogger<SmtpServer> logger)
public class SmtpServer<TSession>(IServiceScopeFactory scopeFactory, ILogger<SmtpServer<TSession>> logger)
where TSession : SmtpSessionBase
{
private readonly ILogger<SmtpServer> _logger = logger;
private readonly IServiceScopeFactory _scopeFactory = scopeFactory;
protected readonly IServiceScopeFactory _scopeFactory = scopeFactory;
protected readonly ILogger<SmtpServer<TSession>> _logger = logger;
public bool IsRunning { get; private set; } = false;
public async Task RunAsync(SmtpConfig config, CancellationToken cancellationToken)
{
if (IsRunning)
if (IsRunning)
return;
var ep = new IPEndPoint(IPAddress.Parse(config.Host), config.Port);
@ -48,7 +49,7 @@ public class SmtpServer(IServiceScopeFactory scopeFactory, ILogger<SmtpServer> l
_logger.LogError(ex.ToString());
}
}
// set listener state
IsRunning = false;
}
@ -57,7 +58,7 @@ public class SmtpServer(IServiceScopeFactory scopeFactory, ILogger<SmtpServer> l
{
// create session
using var scope = _scopeFactory.CreateScope();
var session = scope.ServiceProvider.GetRequiredService<SmtpSession>();
var session = scope.ServiceProvider.GetRequiredService<TSession>();
try
{

View file

@ -1,21 +1,18 @@
using Microsoft.Extensions.Logging;
using SmtpRelay.Graph;
using System.Diagnostics.CodeAnalysis;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace SmtpRelay;
public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> logger)
public abstract partial class SmtpSessionBase(ILogger<SmtpSessionBase> logger)
{
private static byte[] CrlnSpan { get; } = Encoding.ASCII.GetBytes("\r\n");
private static byte[] DataDelimiterSpan { get; } = Encoding.ASCII.GetBytes("\r\n.\r\n");
private readonly GraphSender _graphSender = graphSender;
private readonly ILogger<SmtpSession> _logger = logger;
private readonly List<string> _rcptTo = [];
protected readonly ILogger<SmtpSessionBase> _logger = logger;
protected readonly List<string> _rcptTo = [];
private bool _ehlo = false;
private bool _authenticated = false;
@ -86,6 +83,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
_logger.LogInformation("[{REP}] Disconnected", socket.RemoteEndPoint);
}
protected abstract Task OnProcessDataAsync(ReadOnlyMemory<byte> dataBytes, CancellationToken cancellationToken);
private async Task OnGreeting(Socket socket, SmtpConfig config, CancellationToken cancellationToken)
{
// write default reply message
@ -354,7 +353,7 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
{
var rawData = await ReadToSpanAsync(socket, DataDelimiterSpan, true, cancellationToken);
await ProcessCapturedData(rawData, cancellationToken);
await OnProcessDataAsync(rawData, cancellationToken);
// dot unstuff
//if (response.StartsWith(".."))
@ -600,15 +599,6 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
}
}
private async Task ProcessCapturedData(ReadOnlyMemory<byte> dataBytes, CancellationToken cancellationToken)
{
// save to spool dir
var spoolFile = await SpoolToFileAsync(dataBytes, cancellationToken);
// sent via graph api
await _graphSender.SendAsync(spoolFile, dataBytes, cancellationToken);
}
private static bool ValidateUser(SmtpConfig config, string user, string password)
{
if (!config.Authentication.Any(p => p.Key == user))
@ -636,7 +626,6 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
return spoolFile;
}
[GeneratedRegex(@"^MAIL\s+FROM:\s*<([^>]+)>(?:\s+(.*))?$", RegexOptions.IgnoreCase)]
public static partial Regex MailFromRegex();

View file

@ -8,24 +8,23 @@
"UseWhitelist": true,
"UsePipelining": true,
"Authentication": {
"relay@example.local": "supersecret",
"relay@autohaus-weigl.local": "Secret-Prevent-Whole"
"relay@example.local": "supersecret"
},
"Whitelist": {
"Senders": [
"test@example.local",
"rechnung@autohaus-weigl.de"
"me2@example.local"
],
"Receivers": [
"noreply@example.local",
"%"
]
}
},
"Graph": {
"Bypass": true,
"TenantId": "",
"ClientId": "",
"ClientSecret": "",
"SenderUpn": "",
"Bypass": true
"SenderUpn": ""
}
}

View file

@ -3,9 +3,9 @@
<PropertyGroup>
<Product>SmtpRelay</Product>
<AssemblyName>smtp_relay</AssemblyName>
<AssemblyVersion>2025.10.27.0</AssemblyVersion>
<AssemblyVersion>2025.12.3.0</AssemblyVersion>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<RootNamespace>SmtpRelay</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
@ -17,11 +17,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
</ItemGroup>
<ItemGroup>