From e0b732267bcc4260a4b46b4008b89e1674af015d Mon Sep 17 00:00:00 2001 From: Kevin Kai Berthold Date: Mon, 24 Nov 2025 17:24:27 +0100 Subject: [PATCH] pipeline support --- src/smtprelay/Program.cs | 3 +- .../PublishProfiles/FolderProfile.pubxml | 16 +++++ .../PublishProfiles/FolderProfile1.pubxml | 16 +++++ src/smtprelay/Smtp/SmtpConfig.cs | 1 + src/smtprelay/Smtp/SmtpSession.cs | 64 ++++++++++++++----- src/smtprelay/appsettings.json | 7 +- 6 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 src/smtprelay/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 src/smtprelay/Properties/PublishProfiles/FolderProfile1.pubxml diff --git a/src/smtprelay/Program.cs b/src/smtprelay/Program.cs index 433e43e..b711406 100644 --- a/src/smtprelay/Program.cs +++ b/src/smtprelay/Program.cs @@ -55,7 +55,8 @@ internal partial class Program MaxMessageBytes = host.Configuration.GetSection("Smtp").GetValue("MaxMessageBytes") ?? 10 * 1024 * 1024, Greetings = host.Configuration.GetSection("Smtp").GetValue("Greetings") ?? "OAuth Relay", UseAuthentication = host.Configuration.GetSection("Smtp").GetValue("UseAuthentication") ?? false, - UseWhiteList = host.Configuration.GetSection("Smtp").GetValue("UseWhitelist") ?? false + UseWhiteList = host.Configuration.GetSection("Smtp").GetValue("UseWhitelist") ?? false, + UsePipelining = host.Configuration.GetSection("Smtp").GetValue("UsePipelining") ?? false }; //// fetch authentication entries diff --git a/src/smtprelay/Properties/PublishProfiles/FolderProfile.pubxml b/src/smtprelay/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..a7e4056 --- /dev/null +++ b/src/smtprelay/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + bin\Release\net9.0\publish\win-x64\ + FileSystem + <_TargetId>Folder + net9.0 + win-x64 + true + true + false + + \ No newline at end of file diff --git a/src/smtprelay/Properties/PublishProfiles/FolderProfile1.pubxml b/src/smtprelay/Properties/PublishProfiles/FolderProfile1.pubxml new file mode 100644 index 0000000..d3b6533 --- /dev/null +++ b/src/smtprelay/Properties/PublishProfiles/FolderProfile1.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + bin\Release\net9.0\publish2\ + FileSystem + <_TargetId>Folder + net9.0 + win-x64 + true + false + false + + \ No newline at end of file diff --git a/src/smtprelay/Smtp/SmtpConfig.cs b/src/smtprelay/Smtp/SmtpConfig.cs index 7ce920d..d540344 100644 --- a/src/smtprelay/Smtp/SmtpConfig.cs +++ b/src/smtprelay/Smtp/SmtpConfig.cs @@ -8,6 +8,7 @@ public class SmtpConfig public string Greetings { get; set; } = "Relay"; public TimeSpan ReadTimeout { get; init; } = TimeSpan.FromMinutes(3); public TimeSpan WriteTimeout { get; init; } = TimeSpan.FromMinutes(1); + public bool UsePipelining { get; init; } = false; public bool UseAuthentication { get; set; } = false; public bool UseWhiteList { get; set; } = false; public Dictionary Authentication { get; } = []; diff --git a/src/smtprelay/Smtp/SmtpSession.cs b/src/smtprelay/Smtp/SmtpSession.cs index d3f76be..52e281d 100644 --- a/src/smtprelay/Smtp/SmtpSession.cs +++ b/src/smtprelay/Smtp/SmtpSession.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; using SmtpRelay.Graph; -using System.Net; +using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; using System.Text; using System.Text.Json; @@ -52,6 +52,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l break; var line = Encoding.ASCII.GetString(rawLine); + _logger.LogInformation("[{REP}] > {LINE}", socket.RemoteEndPoint, line); + if (string.IsNullOrWhiteSpace(line)) { await OnEmpty(socket, cancellationToken); @@ -127,6 +129,12 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l await WriteLineAsync(socket, "250-AUTH PLAIN LOGIN", true, cancellationToken); // 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); } @@ -372,7 +380,7 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l private async Task HandleAuthPlainAsync(Socket socket, SmtpConfig config, string? raw, CancellationToken cancellationToken) { - _logger.LogWarning("auth plain"); + _logger.LogCritical("AUTH PLAIN"); // PLAIN == base64 string payloadB64 = raw ?? string.Empty; @@ -385,6 +393,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l var rawLine = await ReadToSpanAsync(socket, CrlnSpan, true, cancellationToken); payloadB64 = Encoding.ASCII.GetString(rawLine ?? throw new InvalidDataException("null")).Trim() ?? ""; + + _logger.LogInformation("[{REP}] > {LINE}", socket.RemoteEndPoint, rawLine); } if (!SmtpHelper.TryBase64(payloadB64, out var bytes)) @@ -406,6 +416,9 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l var user = parts[1]; var pass = parts[2]; + _logger.LogCritical(user); + _logger.LogCritical(pass); + if (ValidateUser(config, user, pass)) { _authenticated = true; @@ -420,7 +433,7 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l private async Task HandleAuthLoginAsync(Socket socket, SmtpConfig config, string? raw, CancellationToken cancellationToken) { - _logger.LogWarning("auth login"); + _logger.LogCritical("AUTH LOGIN"); string? user; string? pass; @@ -442,6 +455,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l var rawLine = await ReadToSpanAsync(socket, CrlnSpan, true, cancellationToken); 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)) { await WriteLineAsync(socket, $"501 5.5.2 Invalid base64", true, cancellationToken); @@ -485,29 +500,42 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l try { + Memory readBuffer = new byte[8192]; + while (!cancellationToken.IsCancellationRequested) { - Memory readBuffer = new byte[8192]; - - var read = await socket.ReceiveAsync(readBuffer, SocketFlags.None, cancellationToken); - if (read == 0) - throw new Exception("Connection Reset"); - - while (_buffer.Length < _bufferOffset + read) - Array.Resize(ref _buffer, _buffer.Length * 2); - + // alloc memory over buffer Memory bufferMemory = new(_buffer); - readBuffer[..read].CopyTo(bufferMemory.Slice(_bufferOffset, read)); - _bufferOffset += read; - // cut out unsed buffer - var unusedBuffer = bufferMemory.Slice(_dataOffset, _bufferOffset); + var unusedBuffer = bufferMemory.Slice(_dataOffset, _bufferOffset - _dataOffset); - // find escape sequence + // 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); + if (read == 0) + throw new Exception("Connection Reset"); + + // increase buffer if needed + while (_buffer.Length < _bufferOffset + read) + Array.Resize(ref _buffer, _buffer.Length * 2); + + bufferMemory = new(_buffer); + readBuffer[..read].CopyTo(bufferMemory.Slice(_bufferOffset, read)); + _bufferOffset += read; + continue; + } // slice out bytes from last escape to new escape var data = unusedBuffer[..(escapeIndex + until.Length)]; @@ -567,6 +595,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger l finally { _writeLock.Release(); + + _logger.LogInformation("[{REP}] < {LINE}", socket.RemoteEndPoint, line); } } diff --git a/src/smtprelay/appsettings.json b/src/smtprelay/appsettings.json index 8264ab6..95ed2bd 100644 --- a/src/smtprelay/appsettings.json +++ b/src/smtprelay/appsettings.json @@ -6,12 +6,15 @@ "MaxMessageBytes": 52428800, "UseAuthentication": true, "UseWhitelist": true, + "UsePipelining": true, "Authentication": { - "relay@example.local": "supersecret" + "relay@example.local": "supersecret", + "relay@autohaus-weigl.local": "Secret-Prevent-Whole" }, "Whitelist": { "Senders": [ - "test@example.local" + "test@example.local", + "rechnung@autohaus-weigl.de" ], "Receivers": [ "%"