pipeline support

This commit is contained in:
Kevin Kai Berthold 2025-11-24 17:24:27 +01:00
parent dfa2fe0de4
commit e0b732267b
6 changed files with 87 additions and 20 deletions

View file

@ -55,7 +55,8 @@ internal partial class Program
MaxMessageBytes = host.Configuration.GetSection("Smtp").GetValue<long?>("MaxMessageBytes") ?? 10 * 1024 * 1024, MaxMessageBytes = host.Configuration.GetSection("Smtp").GetValue<long?>("MaxMessageBytes") ?? 10 * 1024 * 1024,
Greetings = host.Configuration.GetSection("Smtp").GetValue<string?>("Greetings") ?? "OAuth Relay", Greetings = host.Configuration.GetSection("Smtp").GetValue<string?>("Greetings") ?? "OAuth Relay",
UseAuthentication = host.Configuration.GetSection("Smtp").GetValue<bool?>("UseAuthentication") ?? false, UseAuthentication = host.Configuration.GetSection("Smtp").GetValue<bool?>("UseAuthentication") ?? false,
UseWhiteList = host.Configuration.GetSection("Smtp").GetValue<bool?>("UseWhitelist") ?? false UseWhiteList = host.Configuration.GetSection("Smtp").GetValue<bool?>("UseWhitelist") ?? false,
UsePipelining = host.Configuration.GetSection("Smtp").GetValue<bool?>("UsePipelining") ?? false
}; };
//// fetch authentication entries //// fetch authentication entries

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net9.0\publish\win-x64\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net9.0\publish2\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>

View file

@ -8,6 +8,7 @@ public class SmtpConfig
public string Greetings { get; set; } = "Relay"; public string Greetings { get; set; } = "Relay";
public TimeSpan ReadTimeout { get; init; } = TimeSpan.FromMinutes(3); public TimeSpan ReadTimeout { get; init; } = TimeSpan.FromMinutes(3);
public TimeSpan WriteTimeout { get; init; } = TimeSpan.FromMinutes(1); public TimeSpan WriteTimeout { get; init; } = TimeSpan.FromMinutes(1);
public bool UsePipelining { get; init; } = false;
public bool UseAuthentication { get; set; } = false; public bool UseAuthentication { get; set; } = false;
public bool UseWhiteList { get; set; } = false; public bool UseWhiteList { get; set; } = false;
public Dictionary<string, string> Authentication { get; } = []; public Dictionary<string, string> Authentication { get; } = [];

View file

@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SmtpRelay.Graph; using SmtpRelay.Graph;
using System.Net; using System.Diagnostics.CodeAnalysis;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -52,6 +52,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
break; break;
var line = Encoding.ASCII.GetString(rawLine); var line = Encoding.ASCII.GetString(rawLine);
_logger.LogInformation("[{REP}] > {LINE}", socket.RemoteEndPoint, line);
if (string.IsNullOrWhiteSpace(line)) if (string.IsNullOrWhiteSpace(line))
{ {
await OnEmpty(socket, cancellationToken); await OnEmpty(socket, cancellationToken);
@ -127,6 +129,12 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
await WriteLineAsync(socket, "250-AUTH PLAIN LOGIN", true, cancellationToken); await WriteLineAsync(socket, "250-AUTH PLAIN LOGIN", true, cancellationToken);
// write basic pipelining accept (sequencing enforced in code) // write basic pipelining accept (sequencing enforced in code)
if (!config.UsePipelining)
{
await WriteLineAsync(socket, "250 OK", true, cancellationToken);
return;
}
await WriteLineAsync(socket, "250 PIPELINING", true, cancellationToken); await WriteLineAsync(socket, "250 PIPELINING", true, cancellationToken);
} }
@ -372,7 +380,7 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
private async Task HandleAuthPlainAsync(Socket socket, SmtpConfig config, string? raw, CancellationToken cancellationToken) private async Task HandleAuthPlainAsync(Socket socket, SmtpConfig config, string? raw, CancellationToken cancellationToken)
{ {
_logger.LogWarning("auth plain"); _logger.LogCritical("AUTH PLAIN");
// PLAIN == base64 // PLAIN == base64
string payloadB64 = raw ?? string.Empty; string payloadB64 = raw ?? string.Empty;
@ -385,6 +393,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
var rawLine = await ReadToSpanAsync(socket, CrlnSpan, true, cancellationToken); var rawLine = await ReadToSpanAsync(socket, CrlnSpan, true, cancellationToken);
payloadB64 = Encoding.ASCII.GetString(rawLine ?? throw new InvalidDataException("null")).Trim() ?? ""; payloadB64 = Encoding.ASCII.GetString(rawLine ?? throw new InvalidDataException("null")).Trim() ?? "";
_logger.LogInformation("[{REP}] > {LINE}", socket.RemoteEndPoint, rawLine);
} }
if (!SmtpHelper.TryBase64(payloadB64, out var bytes)) if (!SmtpHelper.TryBase64(payloadB64, out var bytes))
@ -406,6 +416,9 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
var user = parts[1]; var user = parts[1];
var pass = parts[2]; var pass = parts[2];
_logger.LogCritical(user);
_logger.LogCritical(pass);
if (ValidateUser(config, user, pass)) if (ValidateUser(config, user, pass))
{ {
_authenticated = true; _authenticated = true;
@ -420,7 +433,7 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
private async Task HandleAuthLoginAsync(Socket socket, SmtpConfig config, string? raw, CancellationToken cancellationToken) private async Task HandleAuthLoginAsync(Socket socket, SmtpConfig config, string? raw, CancellationToken cancellationToken)
{ {
_logger.LogWarning("auth login"); _logger.LogCritical("AUTH LOGIN");
string? user; string? user;
string? pass; string? pass;
@ -442,6 +455,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
var rawLine = await ReadToSpanAsync(socket, CrlnSpan, true, cancellationToken); var rawLine = await ReadToSpanAsync(socket, CrlnSpan, true, cancellationToken);
var response = Encoding.ASCII.GetString(rawLine ?? throw new InvalidDataException("null")).Trim() ?? ""; var response = Encoding.ASCII.GetString(rawLine ?? throw new InvalidDataException("null")).Trim() ?? "";
_logger.LogInformation("[{REP}] > {LINE}", socket.RemoteEndPoint, rawLine);
if (!SmtpHelper.TryBase64(response, out var ub)) if (!SmtpHelper.TryBase64(response, out var ub))
{ {
await WriteLineAsync(socket, $"501 5.5.2 Invalid base64", true, cancellationToken); await WriteLineAsync(socket, $"501 5.5.2 Invalid base64", true, cancellationToken);
@ -484,30 +499,43 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
await _readLock.WaitAsync(cancellationToken); await _readLock.WaitAsync(cancellationToken);
try try
{
while (!cancellationToken.IsCancellationRequested)
{ {
Memory<byte> readBuffer = new byte[8192]; Memory<byte> readBuffer = new byte[8192];
while (!cancellationToken.IsCancellationRequested)
{
// alloc memory over buffer
Memory<byte> bufferMemory = new(_buffer);
// cut out unsed buffer
var unusedBuffer = bufferMemory.Slice(_dataOffset, _bufferOffset - _dataOffset);
// DEBUG ONLY (no aot)
//_logger.LogTrace(_dataOffset.ToString());
//_logger.LogTrace(_bufferOffset.ToString());
//var dbgLine = Encoding.ASCII.GetString(unusedBuffer.Span);
//_logger.LogTrace(JsonSerializer.Serialize(dbgLine));
// find escape sequence if theres more left, before fetching new data
var escapeIndex = unusedBuffer.Span.IndexOf(until.Span);
if (escapeIndex == -1)
{
// try fetch new data
var read = await socket.ReceiveAsync(readBuffer, SocketFlags.None, cancellationToken); var read = await socket.ReceiveAsync(readBuffer, SocketFlags.None, cancellationToken);
if (read == 0) if (read == 0)
throw new Exception("Connection Reset"); throw new Exception("Connection Reset");
// increase buffer if needed
while (_buffer.Length < _bufferOffset + read) while (_buffer.Length < _bufferOffset + read)
Array.Resize(ref _buffer, _buffer.Length * 2); Array.Resize(ref _buffer, _buffer.Length * 2);
Memory<byte> bufferMemory = new(_buffer); bufferMemory = new(_buffer);
readBuffer[..read].CopyTo(bufferMemory.Slice(_bufferOffset, read)); readBuffer[..read].CopyTo(bufferMemory.Slice(_bufferOffset, read));
_bufferOffset += read; _bufferOffset += read;
// cut out unsed buffer
var unusedBuffer = bufferMemory.Slice(_dataOffset, _bufferOffset);
// find escape sequence
var escapeIndex = unusedBuffer.Span.IndexOf(until.Span);
if (escapeIndex == -1)
continue; continue;
}
// slice out bytes from last escape to new escape // slice out bytes from last escape to new escape
var data = unusedBuffer[..(escapeIndex + until.Length)]; var data = unusedBuffer[..(escapeIndex + until.Length)];
@ -567,6 +595,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
finally finally
{ {
_writeLock.Release(); _writeLock.Release();
_logger.LogInformation("[{REP}] < {LINE}", socket.RemoteEndPoint, line);
} }
} }

View file

@ -6,12 +6,15 @@
"MaxMessageBytes": 52428800, "MaxMessageBytes": 52428800,
"UseAuthentication": true, "UseAuthentication": true,
"UseWhitelist": true, "UseWhitelist": true,
"UsePipelining": true,
"Authentication": { "Authentication": {
"relay@example.local": "supersecret" "relay@example.local": "supersecret",
"relay@autohaus-weigl.local": "Secret-Prevent-Whole"
}, },
"Whitelist": { "Whitelist": {
"Senders": [ "Senders": [
"test@example.local" "test@example.local",
"rechnung@autohaus-weigl.de"
], ],
"Receivers": [ "Receivers": [
"%" "%"