.net update, implemented base session, added test client
This commit is contained in:
parent
e0b732267b
commit
7b147569e0
11 changed files with 371 additions and 41 deletions
|
|
@ -1,12 +1,16 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 17.9.34902.65
|
VisualStudioVersion = 18.0.11222.15 d18.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7162BB38-DF40-4438-827C-05A55268EB23}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7162BB38-DF40-4438-827C-05A55268EB23}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "smtprelay", "src\smtprelay\smtprelay.csproj", "{F1834A4A-8E76-61A2-D639-D02937B8885C}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "smtprelay", "src\smtprelay\smtprelay.csproj", "{F1834A4A-8E76-61A2-D639-D02937B8885C}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8B4B0C4C-3F23-4418-91B4-C143295838F5}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "smtpclient", "tests\smtpclient\smtpclient.csproj", "{19058057-D6D9-980C-0359-AC3A630D5EAD}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -17,12 +21,17 @@ Global
|
||||||
{F1834A4A-8E76-61A2-D639-D02937B8885C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F1834A4A-8E76-61A2-D639-D02937B8885C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F1834A4A-8E76-61A2-D639-D02937B8885C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F1834A4A-8E76-61A2-D639-D02937B8885C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F1834A4A-8E76-61A2-D639-D02937B8885C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F1834A4A-8E76-61A2-D639-D02937B8885C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{19058057-D6D9-980C-0359-AC3A630D5EAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{19058057-D6D9-980C-0359-AC3A630D5EAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{19058057-D6D9-980C-0359-AC3A630D5EAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{19058057-D6D9-980C-0359-AC3A630D5EAD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{F1834A4A-8E76-61A2-D639-D02937B8885C} = {7162BB38-DF40-4438-827C-05A55268EB23}
|
{F1834A4A-8E76-61A2-D639-D02937B8885C} = {7162BB38-DF40-4438-827C-05A55268EB23}
|
||||||
|
{19058057-D6D9-980C-0359-AC3A630D5EAD} = {8B4B0C4C-3F23-4418-91B4-C143295838F5}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {91A1FDC3-5235-4A64-AFB8-BF772349C4E7}
|
SolutionGuid = {91A1FDC3-5235-4A64-AFB8-BF772349C4E7}
|
||||||
|
|
|
||||||
31
src/smtprelay/Graph/GraphSmtpSession.cs
Normal file
31
src/smtprelay/Graph/GraphSmtpSession.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -136,8 +136,8 @@ internal partial class Program
|
||||||
services.AddSingleton(graphConfig);
|
services.AddSingleton(graphConfig);
|
||||||
|
|
||||||
// register smtp instances
|
// register smtp instances
|
||||||
services.AddSingleton<SmtpServer>();
|
services.AddSingleton<SmtpServer<GraphSmtpSession>>();
|
||||||
services.AddScoped<SmtpSession>();
|
services.AddScoped<GraphSmtpSession>();
|
||||||
|
|
||||||
// register smtp service
|
// register smtp service
|
||||||
services.AddHostedService<Service>();
|
services.AddHostedService<Service>();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SmtpRelay.Graph;
|
||||||
|
|
||||||
namespace SmtpRelay;
|
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 SmtpConfig _config = config;
|
||||||
private readonly ILogger<Service> _logger = logger;
|
private readonly ILogger<Service> _logger = logger;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,17 @@ using System.Net.Sockets;
|
||||||
|
|
||||||
namespace SmtpRelay;
|
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;
|
protected readonly IServiceScopeFactory _scopeFactory = scopeFactory;
|
||||||
private readonly IServiceScopeFactory _scopeFactory = scopeFactory;
|
protected readonly ILogger<SmtpServer<TSession>> _logger = logger;
|
||||||
|
|
||||||
public bool IsRunning { get; private set; } = false;
|
public bool IsRunning { get; private set; } = false;
|
||||||
|
|
||||||
public async Task RunAsync(SmtpConfig config, CancellationToken cancellationToken)
|
public async Task RunAsync(SmtpConfig config, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (IsRunning)
|
if (IsRunning)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var ep = new IPEndPoint(IPAddress.Parse(config.Host), config.Port);
|
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());
|
_logger.LogError(ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set listener state
|
// set listener state
|
||||||
IsRunning = false;
|
IsRunning = false;
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +58,7 @@ public class SmtpServer(IServiceScopeFactory scopeFactory, ILogger<SmtpServer> l
|
||||||
{
|
{
|
||||||
// create session
|
// create session
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var session = scope.ServiceProvider.GetRequiredService<SmtpSession>();
|
var session = scope.ServiceProvider.GetRequiredService<TSession>();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SmtpRelay.Graph;
|
using SmtpRelay.Graph;
|
||||||
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.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace SmtpRelay;
|
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[] CrlnSpan { get; } = Encoding.ASCII.GetBytes("\r\n");
|
||||||
private static byte[] DataDelimiterSpan { get; } = Encoding.ASCII.GetBytes("\r\n.\r\n");
|
private static byte[] DataDelimiterSpan { get; } = Encoding.ASCII.GetBytes("\r\n.\r\n");
|
||||||
|
|
||||||
private readonly GraphSender _graphSender = graphSender;
|
protected readonly ILogger<SmtpSessionBase> _logger = logger;
|
||||||
private readonly ILogger<SmtpSession> _logger = logger;
|
protected readonly List<string> _rcptTo = [];
|
||||||
private readonly List<string> _rcptTo = [];
|
|
||||||
|
|
||||||
private bool _ehlo = false;
|
private bool _ehlo = false;
|
||||||
private bool _authenticated = false;
|
private bool _authenticated = false;
|
||||||
|
|
@ -86,6 +83,8 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
|
||||||
_logger.LogInformation("[{REP}] Disconnected", socket.RemoteEndPoint);
|
_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)
|
private async Task OnGreeting(Socket socket, SmtpConfig config, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// write default reply message
|
// 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);
|
var rawData = await ReadToSpanAsync(socket, DataDelimiterSpan, true, cancellationToken);
|
||||||
|
|
||||||
await ProcessCapturedData(rawData, cancellationToken);
|
await OnProcessDataAsync(rawData, cancellationToken);
|
||||||
|
|
||||||
// dot unstuff
|
// dot unstuff
|
||||||
//if (response.StartsWith(".."))
|
//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)
|
private static bool ValidateUser(SmtpConfig config, string user, string password)
|
||||||
{
|
{
|
||||||
if (!config.Authentication.Any(p => p.Key == user))
|
if (!config.Authentication.Any(p => p.Key == user))
|
||||||
|
|
@ -636,7 +626,6 @@ public partial class SmtpSession(GraphSender graphSender, ILogger<SmtpSession> l
|
||||||
return spoolFile;
|
return spoolFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[GeneratedRegex(@"^MAIL\s+FROM:\s*<([^>]+)>(?:\s+(.*))?$", RegexOptions.IgnoreCase)]
|
[GeneratedRegex(@"^MAIL\s+FROM:\s*<([^>]+)>(?:\s+(.*))?$", RegexOptions.IgnoreCase)]
|
||||||
public static partial Regex MailFromRegex();
|
public static partial Regex MailFromRegex();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,24 +8,23 @@
|
||||||
"UseWhitelist": true,
|
"UseWhitelist": true,
|
||||||
"UsePipelining": 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",
|
"me2@example.local"
|
||||||
"rechnung@autohaus-weigl.de"
|
|
||||||
],
|
],
|
||||||
"Receivers": [
|
"Receivers": [
|
||||||
|
"noreply@example.local",
|
||||||
"%"
|
"%"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Graph": {
|
"Graph": {
|
||||||
|
"Bypass": true,
|
||||||
"TenantId": "",
|
"TenantId": "",
|
||||||
"ClientId": "",
|
"ClientId": "",
|
||||||
"ClientSecret": "",
|
"ClientSecret": "",
|
||||||
"SenderUpn": "",
|
"SenderUpn": ""
|
||||||
"Bypass": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Product>SmtpRelay</Product>
|
<Product>SmtpRelay</Product>
|
||||||
<AssemblyName>smtp_relay</AssemblyName>
|
<AssemblyName>smtp_relay</AssemblyName>
|
||||||
<AssemblyVersion>2025.10.27.0</AssemblyVersion>
|
<AssemblyVersion>2025.12.3.0</AssemblyVersion>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<RootNamespace>SmtpRelay</RootNamespace>
|
<RootNamespace>SmtpRelay</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||||
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
276
tests/smtpclient/Program.cs
Normal file
276
tests/smtpclient/Program.cs
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
using System.Data;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Mail;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SmtpClient;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static string? _host = null;
|
||||||
|
private static string? _username = null;
|
||||||
|
private static string? _password = null;
|
||||||
|
private static string? _from = null;
|
||||||
|
private static List<string>? _rcpts = [];
|
||||||
|
private static List<string>? _rcptsCc = [];
|
||||||
|
private static string? _subject = null;
|
||||||
|
private static string? _body = null;
|
||||||
|
private static bool _secure = false;
|
||||||
|
|
||||||
|
static async Task Main()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Relay Test Client");
|
||||||
|
|
||||||
|
Console.WriteLine("\nSelect Input:");
|
||||||
|
Console.WriteLine("\t1-Static");
|
||||||
|
Console.WriteLine("\t2-Interactive");
|
||||||
|
|
||||||
|
switch (Console.ReadKey().Key)
|
||||||
|
{
|
||||||
|
case ConsoleKey.D1:
|
||||||
|
case ConsoleKey.NumPad1:
|
||||||
|
StaticInput();
|
||||||
|
break;
|
||||||
|
case ConsoleKey.D2:
|
||||||
|
case ConsoleKey.NumPad2:
|
||||||
|
InteractiveInput();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("\nSelect Method:");
|
||||||
|
Console.WriteLine("\t1-Single Mail");
|
||||||
|
Console.WriteLine("\t2-Single Mail (Raw Pipelined)");
|
||||||
|
Console.WriteLine("\t3-Mutli Mail");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (Console.ReadKey(true).Key)
|
||||||
|
{
|
||||||
|
case ConsoleKey.D1:
|
||||||
|
case ConsoleKey.NumPad1:
|
||||||
|
await SendAsync();
|
||||||
|
break;
|
||||||
|
case ConsoleKey.D2:
|
||||||
|
case ConsoleKey.NumPad2:
|
||||||
|
await SendPipelinedAsync();
|
||||||
|
break;
|
||||||
|
case ConsoleKey.D3:
|
||||||
|
case ConsoleKey.NumPad3:
|
||||||
|
await SendMultiAsync();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Console.WriteLine("\nUndefined Method");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void StaticInput()
|
||||||
|
{
|
||||||
|
_host = "localhost:2525"?.Trim();
|
||||||
|
_secure = false;
|
||||||
|
_username = "relay@example.local";
|
||||||
|
_password = "supersecret";
|
||||||
|
_from = "me@example.local"?.Trim();
|
||||||
|
_rcpts = "noreply@example.local"?.Trim()?.Split(';').ToList();
|
||||||
|
_rcptsCc = [];
|
||||||
|
_subject = "Testnachricht";
|
||||||
|
_body = "Das ist eine Testnachricht.";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InteractiveInput()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nEnter Relay Host: ");
|
||||||
|
_host = Console.ReadLine()?.Trim();
|
||||||
|
|
||||||
|
Console.WriteLine("\nEnter Username (Optional): ");
|
||||||
|
_username = Console.ReadLine()?.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_username))
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nEnter Password: ");
|
||||||
|
_password = Console.ReadLine()?.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("\nEnter Sender:");
|
||||||
|
_from = Console.ReadLine()?.Trim();
|
||||||
|
|
||||||
|
Console.WriteLine("\nEnter Receiver (delimit with ';'):");
|
||||||
|
_rcpts = Console.ReadLine()?.Trim()?.Split(';').ToList();
|
||||||
|
|
||||||
|
Console.WriteLine("\nEnter Cc Receiver (delimit with ';'):");
|
||||||
|
_rcptsCc = Console.ReadLine()?.Trim()?.Split(';').ToList();
|
||||||
|
|
||||||
|
Console.WriteLine("\nEnter Subject:");
|
||||||
|
_subject = Console.ReadLine()?.Trim();
|
||||||
|
|
||||||
|
Console.WriteLine("\nEnter Body:");
|
||||||
|
_body = Console.ReadLine()?.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task SendAsync()
|
||||||
|
{
|
||||||
|
var from = new MailAddress(_from!);
|
||||||
|
var to = _rcpts!.Select(p => new MailAddress(p)).ToList();
|
||||||
|
var cc = _rcptsCc!.Select(p => new MailAddress(p)).ToList();
|
||||||
|
|
||||||
|
var firstTo = to.First();
|
||||||
|
to.Remove(firstTo);
|
||||||
|
|
||||||
|
using var message = new MailMessage(from, firstTo)
|
||||||
|
{
|
||||||
|
Priority = MailPriority.Normal,
|
||||||
|
Subject = _subject ?? "Undefined",
|
||||||
|
IsBodyHtml = false,
|
||||||
|
Body = _body ?? string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var x in to)
|
||||||
|
message.To.Add(x);
|
||||||
|
|
||||||
|
foreach (var x in cc)
|
||||||
|
message.CC.Add(x);
|
||||||
|
|
||||||
|
message.Attachments.Add(new Attachment("attachment.txt"));
|
||||||
|
|
||||||
|
var credential = new NetworkCredential();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_username))
|
||||||
|
{
|
||||||
|
credential.UserName = _username;
|
||||||
|
credential.Password = _password;
|
||||||
|
}
|
||||||
|
|
||||||
|
var host = _host;
|
||||||
|
var port = 25;
|
||||||
|
|
||||||
|
if (_host!.Contains(':'))
|
||||||
|
{
|
||||||
|
host = _host.Split(':')[0];
|
||||||
|
port = int.Parse(_host.Split(":")[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var smtp = new System.Net.Mail.SmtpClient(host, port)
|
||||||
|
{
|
||||||
|
UseDefaultCredentials = false,
|
||||||
|
Credentials = credential,
|
||||||
|
EnableSsl = _secure,
|
||||||
|
DeliveryMethod = SmtpDeliveryMethod.Network
|
||||||
|
};
|
||||||
|
|
||||||
|
await smtp.SendMailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task SendMultiAsync()
|
||||||
|
{
|
||||||
|
var from = new MailAddress(_from!);
|
||||||
|
var to = _rcpts!.Select(p => new MailAddress(p)).ToList();
|
||||||
|
var cc = _rcptsCc!.Select(p => new MailAddress(p)).ToList();
|
||||||
|
|
||||||
|
var firstTo = to.First();
|
||||||
|
to.Remove(firstTo);
|
||||||
|
|
||||||
|
using var message = new MailMessage(from, firstTo)
|
||||||
|
{
|
||||||
|
Priority = MailPriority.Normal,
|
||||||
|
Subject = _subject ?? "Undefined",
|
||||||
|
IsBodyHtml = false,
|
||||||
|
Body = _body ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var x in to)
|
||||||
|
message.To.Add(x);
|
||||||
|
|
||||||
|
foreach (var x in cc)
|
||||||
|
message.CC.Add(x);
|
||||||
|
|
||||||
|
message.Attachments.Add(new Attachment("attachment.txt"));
|
||||||
|
|
||||||
|
var credential = new NetworkCredential();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_username))
|
||||||
|
{
|
||||||
|
credential.UserName = _username;
|
||||||
|
credential.Password = _password;
|
||||||
|
}
|
||||||
|
|
||||||
|
var host = _host;
|
||||||
|
var port = 25;
|
||||||
|
|
||||||
|
if (_host!.Contains(':'))
|
||||||
|
{
|
||||||
|
host = _host.Split(':')[0];
|
||||||
|
port = int.Parse(_host.Split(":")[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var smtp = new System.Net.Mail.SmtpClient(host, port)
|
||||||
|
{
|
||||||
|
UseDefaultCredentials = false,
|
||||||
|
Credentials = credential,
|
||||||
|
EnableSsl = _secure,
|
||||||
|
DeliveryMethod = SmtpDeliveryMethod.Network
|
||||||
|
};
|
||||||
|
|
||||||
|
await smtp.SendMailAsync(message);
|
||||||
|
await smtp.SendMailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task SendPipelinedAsync()
|
||||||
|
{
|
||||||
|
var raw = "EHLO SmtpClient\r\n";
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_username))
|
||||||
|
{
|
||||||
|
raw += $"AUTH PLAIN {Convert.ToBase64String(Encoding.UTF8.GetBytes($"\0{_username}\0{_password}"))}\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
raw += $"MAIL FROM:<{_from}> SIZE=512\r\n";
|
||||||
|
|
||||||
|
foreach (var rcpt in _rcpts!)
|
||||||
|
{
|
||||||
|
raw += $"RCPT TO:<{rcpt}>\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
raw += "DATA\r\n";
|
||||||
|
raw += "MIME-Version: 1.0\r\n";
|
||||||
|
raw += $"From: {_from}\r\n";
|
||||||
|
raw += $"To: {string.Join(", ", _rcpts!)}\r\n";
|
||||||
|
|
||||||
|
if (_rcptsCc != null && _rcptsCc.Count > 0)
|
||||||
|
{
|
||||||
|
raw += $"Cc: {string.Join(", ", _rcptsCc!)}\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
raw += "Date: 29 Oct 2025 21:10:56 +0100\r\n";
|
||||||
|
raw += $"Subject:{_subject}\r\n";
|
||||||
|
raw += "\r\n" ;
|
||||||
|
raw += $"{_body}";
|
||||||
|
raw += "\r\n.\r\n";
|
||||||
|
raw += "QUIT\r\n";
|
||||||
|
|
||||||
|
var bytes = Encoding.ASCII.GetBytes(raw);
|
||||||
|
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
|
||||||
|
var host = _host;
|
||||||
|
var port = 25;
|
||||||
|
|
||||||
|
if (_host!.Contains(':'))
|
||||||
|
{
|
||||||
|
host = _host.Split(':')[0];
|
||||||
|
port = int.Parse(_host.Split(":")[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sock.ConnectAsync(host!, port);
|
||||||
|
await sock.SendAsync(bytes);
|
||||||
|
|
||||||
|
Console.WriteLine("wait 3 seconds...");
|
||||||
|
await Task.Delay(3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
tests/smtpclient/attachment.txt
Normal file
1
tests/smtpclient/attachment.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
test
|
||||||
23
tests/smtpclient/smtpclient.csproj
Normal file
23
tests/smtpclient/smtpclient.csproj
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Product>SmtpClient</Product>
|
||||||
|
<AssemblyName>smtp_client</AssemblyName>
|
||||||
|
<AssemblyVersion>2025.12.3.0</AssemblyVersion>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<RootNamespace>SmtpClient</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<SatelliteResourceLanguages>none</SatelliteResourceLanguages>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="attachment.txt">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue