From 3f9f0e7a9eb37ca696487198d6e6c992fa813db4 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Mon, 25 Nov 2019 17:11:30 +0100 Subject: [PATCH 1/9] Add SmtpClient.SendMailAsync overloads with cancellation --- .../System.Net.Mail/ref/System.Net.Mail.cs | 2 + .../src/System/Net/Mail/SmtpClient.cs | 61 ++++++++++++++++--- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Net.Mail/ref/System.Net.Mail.cs b/src/libraries/System.Net.Mail/ref/System.Net.Mail.cs index d1b280aec9489..b3c1f32996dca 100644 --- a/src/libraries/System.Net.Mail/ref/System.Net.Mail.cs +++ b/src/libraries/System.Net.Mail/ref/System.Net.Mail.cs @@ -186,6 +186,8 @@ public void SendAsync(string from, string recipients, string subject, string bod public void SendAsyncCancel() { } public System.Threading.Tasks.Task SendMailAsync(System.Net.Mail.MailMessage message) { throw null; } public System.Threading.Tasks.Task SendMailAsync(string from, string recipients, string subject, string body) { throw null; } + public System.Threading.Tasks.Task SendMailAsync(System.Net.Mail.MailMessage message, System.Threading.CancellationToken cancellationToken) { throw null; } + public System.Threading.Tasks.Task SendMailAsync(string from, string recipients, string subject, string body, System.Threading.CancellationToken cancellationToken) { throw null; } } public enum SmtpDeliveryFormat { diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs index d67a2660eda60..3ddd9b283e7fc 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs @@ -786,17 +786,35 @@ public void SendAsyncCancel() public Task SendMailAsync(string from, string recipients, string subject, string body) { var message = new MailMessage(from, recipients, subject, body); - return SendMailAsync(message); + return SendMailAsync(message, cancellationToken: default); } public Task SendMailAsync(MailMessage message) { + return SendMailAsync(message, cancellationToken: default); + } + + public Task SendMailAsync(string from, string recipients, string subject, string body, CancellationToken cancellationToken) + { + var message = new MailMessage(from, recipients, subject, body); + return SendMailAsync(message, cancellationToken); + } + + public Task SendMailAsync(MailMessage message, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + // Create a TaskCompletionSource to represent the operation - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + CancellationTokenRegistration ctr = default; // Register a handler that will transfer completion results to the TCS Task SendCompletedEventHandler handler = null; - handler = (sender, e) => HandleCompletion(tcs, e, handler); + handler = (sender, e) => HandleCompletion(tcs, ctr, e, handler); SendCompleted += handler; // Start the async operation. @@ -810,20 +828,45 @@ public Task SendMailAsync(MailMessage message) throw; } + // Only register on the CT if HandleCompletion hasn't started to ensure the CTR is disposed + bool lockTaken = false; + try + { + Monitor.TryEnter(tcs, ref lockTaken); + if (lockTaken && !tcs.Task.IsCompleted) + { + ctr = cancellationToken.Register(s => + { + ((SmtpClient)s).SendAsyncCancel(); + }, this); + } + } + finally + { + if (lockTaken) Monitor.Exit(tcs); + } + // Return the task to represent the asynchronous operation return tcs.Task; } - private void HandleCompletion(TaskCompletionSource tcs, AsyncCompletedEventArgs e, SendCompletedEventHandler handler) + private void HandleCompletion(TaskCompletionSource tcs, CancellationTokenRegistration ctr, AsyncCompletedEventArgs e, SendCompletedEventHandler handler) { if (e.UserState == tcs) { - try { SendCompleted -= handler; } - finally + lock (tcs) { - if (e.Error != null) tcs.TrySetException(e.Error); - else if (e.Cancelled) tcs.TrySetCanceled(); - else tcs.TrySetResult(null); + try + { + SendCompleted -= handler; + ctr.Dispose(); + } + finally + { + if (e.Error != null) tcs.TrySetException(e.Error); + else if (e.Cancelled) tcs.TrySetCanceled(); + else tcs.TrySetResult(null); + } } } } From 11fe7a1a354e4d01d366ec21683cfdd0070d34c7 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Tue, 26 Nov 2019 13:25:47 +0100 Subject: [PATCH 2/9] Rework mock SmtpClient tests --- .../tests/Functional/MockSmtpServer.cs | 279 ++++++++++++++++++ .../tests/Functional/SmtpClientTest.cs | 129 +++----- .../tests/Functional/SmtpServer.cs | 150 ---------- .../System.Net.Mail.Functional.Tests.csproj | 2 +- 4 files changed, 324 insertions(+), 236 deletions(-) create mode 100644 src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs delete mode 100644 src/libraries/System.Net.Mail/tests/Functional/SmtpServer.cs diff --git a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs new file mode 100644 index 0000000000000..06ddbd449b041 --- /dev/null +++ b/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Mail; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Systen.Net.Mail.Tests +{ + public class MockSmtpServer : IDisposable + { + private static readonly ReadOnlyMemory MessageTerminator = new byte[] { (byte)'\r', (byte)'\n' }; + private static readonly ReadOnlyMemory BodyTerminator = new byte[] { (byte)'\r', (byte)'\n', (byte)'.', (byte)'\r', (byte)'\n' }; + + public bool ReceiveMultipleConnections = false; + public bool SupportSmtpUTF8 = false; + + private bool _disposed = false; + private readonly Socket _listenSocket; + private readonly ConcurrentBag _socketsToDispose; + private long _messageCounter = new Random().Next(1000, 2000); + + public readonly int Port; + public SmtpClient CreateClient() => new SmtpClient("localhost", Port); + + public Action OnConnected; + public Action OnHelloReceived; + public Action OnCommandReceived; + public Action OnUnknownCommand; + public Action OnQuitReceived; + + public string ClientDomain { get; private set; } + public string MailFrom { get; private set; } + public string MailTo { get; private set; } + public string UsernamePassword { get; private set; } + public string Username { get; private set; } + public string Password { get; private set; } + public string AuthMethodUsed { get; private set; } + public ParsedMailMessage Message { get; private set; } + + public int ConnectionCount { get; private set; } + public int MessagesReceived { get; private set; } + + public MockSmtpServer() + { + _socketsToDispose = new ConcurrentBag(); + _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _socketsToDispose.Add(_listenSocket); + + _listenSocket.Bind(new IPEndPoint(IPAddress.Any, 0)); + Port = ((IPEndPoint)_listenSocket.LocalEndPoint).Port; + _listenSocket.Listen(1); + + _ = Task.Run(async () => + { + do + { + var socket = await _listenSocket.AcceptAsync(); + _socketsToDispose.Add(socket); + ConnectionCount++; + _ = Task.Run(async () => await HandleConnectionAsync(socket)); + } + while (ReceiveMultipleConnections); + }); + } + + private async Task HandleConnectionAsync(Socket socket) + { + var buffer = new byte[1024].AsMemory(); + + async ValueTask ReceiveMessageAsync(bool isBody = false) + { + var terminator = isBody ? BodyTerminator : MessageTerminator; + int suffix = terminator.Length; + + int received = 0; + do + { + int read = await socket.ReceiveAsync(buffer.Slice(received), SocketFlags.None); + if (read == 0) return null; + received += read; + } + while (received < suffix || !buffer.Slice(received - suffix, suffix).Span.SequenceEqual(terminator.Span)); + + MessagesReceived++; + return Encoding.UTF8.GetString(buffer.Span.Slice(0, received - suffix)); + } + async ValueTask SendMessageAsync(string text) + { + var bytes = buffer.Slice(0, Encoding.UTF8.GetBytes(text, buffer.Span) + 2); + bytes.Span[^2] = (byte)'\r'; + bytes.Span[^1] = (byte)'\n'; + await socket.SendAsync(bytes, SocketFlags.None); + } + + try + { + OnConnected?.Invoke(socket); + await SendMessageAsync("220 localhost"); + + string message = await ReceiveMessageAsync(); + Debug.Assert(message.ToLower().StartsWith("helo ") || message.ToLower().StartsWith("ehlo ")); + ClientDomain = message.Substring(5); + OnCommandReceived?.Invoke(message.Substring(0, 4), ClientDomain); + OnHelloReceived?.Invoke(ClientDomain); + + await SendMessageAsync("250-localhost, mock server here"); + if (SupportSmtpUTF8) await SendMessageAsync("250-SMTPUTF8"); + await SendMessageAsync("250 AUTH PLAIN LOGIN"); + + while ((message = await ReceiveMessageAsync()) != null) + { + int colonIndex = message.IndexOf(':'); + string command = colonIndex == -1 ? message : message.Substring(0, colonIndex); + string argument = command.Length == message.Length ? string.Empty : message.Substring(colonIndex + 1).Trim(); + + OnCommandReceived?.Invoke(command, argument); + + if (command.StartsWith("AUTH", StringComparison.OrdinalIgnoreCase)) + { + var parts = command.Split(' '); + Debug.Assert(parts.Length > 1, "Expected an actual auth request"); + + AuthMethodUsed = parts[1]; + + // PLAIN is not supported by SmtpClient + /* + if (parts[1].Equals("PLAIN", StringComparison.OrdinalIgnoreCase)) + { + string base64; + if (parts.Length == 2) + { + await SendMessageAsync("334"); + base64 = await ReceiveMessageAsync(); + } + else + { + base64 = parts[2]; + } + UsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + await SendMessageAsync("235 Authentication successful"); + } + else + */ + if (parts[1].Equals("LOGIN", StringComparison.OrdinalIgnoreCase)) + { + if (parts.Length == 2) + { + await SendMessageAsync("334 VXNlcm5hbWU6"); + Username = Encoding.UTF8.GetString(Convert.FromBase64String(await ReceiveMessageAsync())); + } + else + { + Username = Encoding.UTF8.GetString(Convert.FromBase64String(parts[2])); + } + await SendMessageAsync("334 UGFzc3dvcmQ6"); + Password = Encoding.UTF8.GetString(Convert.FromBase64String(await ReceiveMessageAsync())); + UsernamePassword = Username + Password; + await SendMessageAsync("235 Authentication successful"); + } + else await SendMessageAsync("504 scheme not supported"); + continue; + } + + switch (command.ToUpper()) + { + case "MAIL FROM": + MailFrom = argument; + await SendMessageAsync("250 Ok"); + break; + + case "RCPT TO": + MailTo = argument; + await SendMessageAsync("250 Ok"); + break; + + case "DATA": + await SendMessageAsync("354 Start mail input; end with ."); + string data = await ReceiveMessageAsync(true); + Message = ParsedMailMessage.Parse(data); + await SendMessageAsync("250 Ok: queued as " + Interlocked.Increment(ref _messageCounter)); + break; + + case "QUIT": + OnQuitReceived?.Invoke(socket); + await SendMessageAsync("221 Bye"); + return; + + default: + OnUnknownCommand?.Invoke(message); + await SendMessageAsync("500 Idk that command"); + break; + } + } + } + catch { } + finally + { + try + { + socket.Shutdown(SocketShutdown.Both); + } + finally + { + socket?.Close(); + } + } + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + foreach (var socket in _socketsToDispose) + { + try + { + socket.Close(); + } + catch { } + } + _socketsToDispose.Clear(); + } + } + + + public class ParsedMailMessage + { + public readonly IReadOnlyDictionary Headers; + public readonly string Body; + + private string GetHeader(string name) => Headers.TryGetValue(name, out string value) ? value : "NOT-PRESENT"; + public string From => GetHeader("From"); + public string To => GetHeader("To"); + public string Subject => GetHeader("Subject"); + + private ParsedMailMessage(Dictionary headers, string body) + { + Headers = headers; + Body = body; + } + + public static ParsedMailMessage Parse(string data) + { + Dictionary headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ReadOnlySpan dataSpan = data; + string body = null; + + while (!dataSpan.IsEmpty) + { + int endOfLine = dataSpan.IndexOf('\n'); + Debug.Assert(endOfLine != -1, "Expected valid \r\n terminated lines"); + var line = dataSpan.Slice(0, endOfLine).TrimEnd('\r'); + + if (line.IsEmpty) + { + body = dataSpan.Slice(endOfLine + 1).TrimEnd(stackalloc char[] { '\r', '\n' }).ToString(); + break; + } + else + { + int colon = line.IndexOf(':'); + Debug.Assert(colon != -1, "Expected a valid header"); + headers.Add(line.Slice(0, colon).Trim().ToString(), line.Slice(colon + 1).Trim().ToString()); + dataSpan = dataSpan.Slice(endOfLine + 1); + } + } + + return new ParsedMailMessage(headers, body); + } + } + } +} diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 2c8bfdfe73dc8..ea3b0be54f469 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -14,6 +14,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Systen.Net.Mail.Tests; using Xunit; namespace System.Net.Mail.Tests @@ -290,29 +291,18 @@ public async Task SendAsync_ServerDoesntExist_Throws() [Fact] public void TestMailDelivery() { - SmtpServer server = new SmtpServer(); - SmtpClient client = new SmtpClient("localhost", server.EndPoint.Port); + using var server = new MockSmtpServer(); + using SmtpClient client = server.CreateClient(); client.Credentials = new NetworkCredential("user", "password"); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - string clientDomain = IPGlobalProperties.GetIPGlobalProperties().HostName.Trim().ToLower(); - try - { - Thread t = new Thread(server.Run); - t.Start(); - client.Send(msg); - t.Join(); + client.Send(msg); - Assert.Equal("", server.MailFrom); - Assert.Equal("", server.MailTo); - Assert.Equal("hello", server.Subject); - Assert.Equal("howdydoo", server.Body); - Assert.Equal(clientDomain, server.ClientDomain); - } - finally - { - server.Stop(); - } + Assert.Equal("", server.MailFrom); + Assert.Equal("", server.MailTo); + Assert.Equal("hello", server.Message.Subject); + Assert.Equal("howdydoo", server.Message.Body); + Assert.Equal(GetClientDomain(), server.ClientDomain); } [Fact] @@ -349,60 +339,38 @@ public void TestZeroTimeout() [InlineData(null)] public async Task TestMailDeliveryAsync(string body) { - SmtpServer server = new SmtpServer(); - SmtpClient client = new SmtpClient("localhost", server.EndPoint.Port); + using var server = new MockSmtpServer(); + using SmtpClient client = server.CreateClient(); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", body); - string clientDomain = IPGlobalProperties.GetIPGlobalProperties().HostName.Trim().ToLower(); - try - { - Thread t = new Thread(server.Run); - t.Start(); - await client.SendMailAsync(msg).TimeoutAfter((int)TimeSpan.FromSeconds(30).TotalMilliseconds); - t.Join(); - - Assert.Equal("", server.MailFrom); - Assert.Equal("", server.MailTo); - Assert.Equal("hello", server.Subject); - Assert.Equal(body ?? "", server.Body); - Assert.Equal(clientDomain, server.ClientDomain); - } - finally - { - server.Stop(); - } + await client.SendMailAsync(msg).TimeoutAfter((int)TimeSpan.FromSeconds(30).TotalMilliseconds); + + Assert.Equal("", server.MailFrom); + Assert.Equal("", server.MailTo); + Assert.Equal("hello", server.Message.Subject); + Assert.Equal(body ?? "", server.Message.Body); + Assert.Equal(GetClientDomain(), server.ClientDomain); } [Fact] public async Task TestCredentialsCopyInAsyncContext() { - SmtpServer server = new SmtpServer(); - SmtpClient client = new SmtpClient("localhost", server.EndPoint.Port); + using var server = new MockSmtpServer(); + using SmtpClient client = server.CreateClient(); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - string clientDomain = IPGlobalProperties.GetIPGlobalProperties().HostName.Trim().ToLower(); CredentialCache cache = new CredentialCache(); - cache.Add("localhost", server.EndPoint.Port, "NTLM", CredentialCache.DefaultNetworkCredentials); + cache.Add("localhost", server.Port, "NTLM", CredentialCache.DefaultNetworkCredentials); client.Credentials = cache; - try - { - Thread t = new Thread(server.Run); - t.Start(); - await client.SendMailAsync(msg); - t.Join(); - - Assert.Equal("", server.MailFrom); - Assert.Equal("", server.MailTo); - Assert.Equal("hello", server.Subject); - Assert.Equal("howdydoo", server.Body); - Assert.Equal(clientDomain, server.ClientDomain); - } - finally - { - server.Stop(); - } + await client.SendMailAsync(msg); + + Assert.Equal("", server.MailFrom); + Assert.Equal("", server.MailTo); + Assert.Equal("hello", server.Message.Subject); + Assert.Equal("howdydoo", server.Message.Body); + Assert.Equal(GetClientDomain(), server.ClientDomain); } @@ -423,13 +391,12 @@ public void SendMail_DeliveryFormat_SubjectEncoded(bool useAsyncSend, bool useSe // If the server does not support `SMTPUTF8` or use `SmtpDeliveryFormat.SevenBit`, the server should received this subject. const string subjectBase64 = "=?utf-8?B?VGVzdCDmtYvor5UgQ29udGFpbiDljIXlkKsgVVRGOA==?="; - SmtpServer server = new SmtpServer(); + using var server = new MockSmtpServer(); + using SmtpClient client = server.CreateClient(); // Setting up Server Support for `SMTPUTF8`. server.SupportSmtpUTF8 = useSmtpUTF8; - SmtpClient client = new SmtpClient("localhost", server.EndPoint.Port); - if (useSevenBit) { // Subject will be encoded by Base64. @@ -444,33 +411,25 @@ public void SendMail_DeliveryFormat_SubjectEncoded(bool useAsyncSend, bool useSe MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", subjectText, "hello \u9ad8\u575a\u679c"); msg.HeadersEncoding = msg.BodyEncoding = msg.SubjectEncoding = System.Text.Encoding.UTF8; - try + if (useAsyncSend) { - Thread t = new Thread(server.Run); - t.Start(); - - if (useAsyncSend) - { - client.SendMailAsync(msg).Wait(); - } - else - { - client.Send(msg); - } + client.SendMailAsync(msg).Wait(); + } + else + { + client.Send(msg); + } - if (useSevenBit || !useSmtpUTF8) - { - Assert.Equal(subjectBase64, server.Subject); - } - else - { - Assert.Equal(subjectText, server.Subject); - } + if (useSevenBit || !useSmtpUTF8) + { + Assert.Equal(subjectBase64, server.Message.Subject); } - finally + else { - server.Stop(); + Assert.Equal(subjectText, server.Message.Subject); } } + + private static string GetClientDomain() => IPGlobalProperties.GetIPGlobalProperties().HostName.Trim(); } } diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpServer.cs deleted file mode 100644 index 403f7c5c03a1d..0000000000000 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpServer.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// See the LICENSE file in the project root for more information. -// -// SmtpServer.cs - Dummy SMTP server used to test SmtpClient -// -// Author: -// Raja R Harinath -// - -using System.Diagnostics; -using System.IO; -using System.Net.Sockets; -using System.Text; - -namespace System.Net.Mail.Tests -{ - public class SmtpServer - { - private string _mailfrom, _mailto, _subject, _body, _clientdomain; - - public string MailFrom => _mailfrom; - public string MailTo => _mailto; - public string Subject => _subject; - public string Body => _body; - public string ClientDomain => _clientdomain; - - private readonly TcpListener _server; - - public IPEndPoint EndPoint - { - get { return (IPEndPoint)_server.LocalEndpoint; } - } - - public bool SupportSmtpUTF8 { get; set; } - - public SmtpServer() - { - IPAddress address = IPAddress.Loopback; - _server = new TcpListener(address, 0); - _server.Start(1); - } - - private static void WriteNS(NetworkStream ns, string s) - { - Trace("response", s); - byte[] bytes = Encoding.ASCII.GetBytes(s); - ns.Write(bytes, 0, bytes.Length); - } - - public void Stop() - { - _server.Stop(); - } - - public void Run() - { - try - { - string s; - using (TcpClient client = _server.AcceptTcpClient()) - { - Trace("connection", EndPoint.Port); - using (NetworkStream ns = client.GetStream()) - { - WriteNS(ns, "220 localhost\r\n"); - using (StreamReader r = new StreamReader(ns, Encoding.UTF8)) - { - while ((s = r.ReadLine()) != null && Dispatch(ns, r, s)) - ; - } - } - } - } - catch (SocketException e) - { - // The _server might have been stopped. - if (e.SocketErrorCode != SocketError.Interrupted) - throw; - } - } - - // return false == terminate - private bool Dispatch(NetworkStream ns, StreamReader r, string s) - { - Trace("command", s); - if (s.Length < 4) - { - WriteNS(ns, "502 Unrecognized\r\n"); - return false; - } - - bool retval = true; - switch (s.Substring(0, 4)) - { - case "HELO": - _clientdomain = s.Substring(5).Trim().ToLower(); - break; - case "EHLO": - _clientdomain = s.Substring(5).Trim().ToLower(); - WriteNS(ns, "250-localhost Hello" + s.Substring(5, s.Length - 5) + "\r\n"); - WriteNS(ns, "250-AUTH PLAIN\r\n"); - if (SupportSmtpUTF8) - { - WriteNS(ns, "250-SMTPUTF8\r\n"); - } - break; - case "QUIT": - WriteNS(ns, "221 Quit\r\n"); - return false; - case "MAIL": - _mailfrom = s.Substring(10); - break; - case "RCPT": - _mailto = s.Substring(8); - break; - case "DATA": - WriteNS(ns, "354 Continue\r\n"); - while ((s = r.ReadLine()) != null) - { - if (s == ".") - break; - - if (s.StartsWith("Subject:")) - { - _subject = s.Substring(9, s.Length - 9); - } - else if (s == "" && _body == null) - { - _body = r.ReadLine(); - } - } - Trace("end of data", s); - retval = (s != null); - break; - default: - WriteNS(ns, "502 Unrecognized\r\n"); - return true; - } - - WriteNS(ns, "250 OK\r\n"); - return retval; - } - - [Conditional("TEST")] - private static void Trace(string key, object value) - { - Console.Error.WriteLine("{0}: {1}", key, value); - } - } -} diff --git a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj index 9bdd31a46ace3..d9058df715bf5 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj +++ b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj @@ -20,7 +20,7 @@ - + Common\System\Diagnostics\Tracing\TestEventListener.cs From e513058e631d6373a885dbdae54bccb1661f4f4a Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Tue, 26 Nov 2019 13:38:40 +0100 Subject: [PATCH 3/9] Verify that SmtpClient uses Auth if available --- .../tests/Functional/MockSmtpServer.cs | 7 ++++++- .../tests/Functional/SmtpClientTest.cs | 18 +++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs index 06ddbd449b041..9fd82995bbefe 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs @@ -18,6 +18,7 @@ public class MockSmtpServer : IDisposable public bool ReceiveMultipleConnections = false; public bool SupportSmtpUTF8 = false; + public bool AdvertiseNtlmAuthSupport = false; private bool _disposed = false; private readonly Socket _listenSocket; @@ -110,7 +111,7 @@ async ValueTask SendMessageAsync(string text) await SendMessageAsync("250-localhost, mock server here"); if (SupportSmtpUTF8) await SendMessageAsync("250-SMTPUTF8"); - await SendMessageAsync("250 AUTH PLAIN LOGIN"); + await SendMessageAsync("250 AUTH PLAIN LOGIN" + (AdvertiseNtlmAuthSupport ? " NTLM" : "")); while ((message = await ReceiveMessageAsync()) != null) { @@ -162,6 +163,10 @@ async ValueTask SendMessageAsync(string text) UsernamePassword = Username + Password; await SendMessageAsync("235 Authentication successful"); } + else if (parts[1].Equals("NTLM", StringComparison.OrdinalIgnoreCase)) + { + await SendMessageAsync("12345 I lied, I can't speak NTLM - here's an invalid response"); + } else await SendMessageAsync("504 scheme not supported"); continue; } diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index ea3b0be54f469..c9b434448ad6a 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -291,9 +291,12 @@ public async Task SendAsync_ServerDoesntExist_Throws() [Fact] public void TestMailDelivery() { + const string Username = "Foo"; + const string Password = "Bar"; + using var server = new MockSmtpServer(); using SmtpClient client = server.CreateClient(); - client.Credentials = new NetworkCredential("user", "password"); + client.Credentials = new NetworkCredential(Username, Password); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); client.Send(msg); @@ -303,6 +306,9 @@ public void TestMailDelivery() Assert.Equal("hello", server.Message.Subject); Assert.Equal("howdydoo", server.Message.Body); Assert.Equal(GetClientDomain(), server.ClientDomain); + Assert.Equal(Username, server.Username); + Assert.Equal(Password, server.Password); + Assert.Equal("login", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); } [Fact] @@ -364,13 +370,11 @@ public async Task TestCredentialsCopyInAsyncContext() client.Credentials = cache; - await client.SendMailAsync(msg); + // The mock server doesn't actually understand NTML, but still advertises support for it + server.AdvertiseNtlmAuthSupport = true; + await Assert.ThrowsAsync(async () => await client.SendMailAsync(msg)); - Assert.Equal("", server.MailFrom); - Assert.Equal("", server.MailTo); - Assert.Equal("hello", server.Message.Subject); - Assert.Equal("howdydoo", server.Message.Body); - Assert.Equal(GetClientDomain(), server.ClientDomain); + Assert.Equal("ntlm", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); } From 4b89dee7791a0accd8a4e39750315081f7a9df22 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Tue, 26 Nov 2019 13:58:00 +0100 Subject: [PATCH 4/9] Add tests for SmtpClient.SendMailAsync using CancellationTokens --- .../tests/Functional/SmtpClientTest.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index c9b434448ad6a..0cbcb28856987 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -434,6 +434,57 @@ public void SendMail_DeliveryFormat_SubjectEncoded(bool useAsyncSend, bool useSe } } + [Fact] + public void SendMailAsync_CanBeCanceled_CancellationToken_SetAlready() + { + using var server = new MockSmtpServer(); + using SmtpClient client = server.CreateClient(); + + CancellationTokenSource cts = new CancellationTokenSource(); + cts.Cancel(); + + var message = new MailMessage("foo@internet.com", "bar@internet.com", "Foo", "Bar"); + + Task sendTask = client.SendMailAsync(message, cts.Token); + + // Tests an implementation detail - if a CT is already set a canceled task will be returned + Assert.True(sendTask.IsCanceled); + } + + [Fact] + public async Task SendMailAsync_CanBeCanceled_CancellationToken() + { + using var server = new MockSmtpServer(); + using SmtpClient client = server.CreateClient(); + + server.ReceiveMultipleConnections = true; + + // The server will introduce some fake latency so that the operation can be canceled before the request completes + ManualResetEvent serverMre = new ManualResetEvent(false); + server.OnConnected += _ => serverMre.WaitOne(); + + CancellationTokenSource cts = new CancellationTokenSource(); + + var message = new MailMessage("foo@internet.com", "bar@internet.com", "Foo", "Bar"); + + Task sendTask = client.SendMailAsync(message, cts.Token); + + cts.Cancel(); + await Task.Delay(500); + serverMre.Set(); + + await Assert.ThrowsAsync(async () => await sendTask); + + // We should still be able to send mail on the SmtpClient instance + await client.SendMailAsync(message); + + Assert.Equal("", server.MailFrom); + Assert.Equal("", server.MailTo); + Assert.Equal("Foo", server.Message.Subject); + Assert.Equal("Bar", server.Message.Body); + Assert.Equal(GetClientDomain(), server.ClientDomain); + } + private static string GetClientDomain() => IPGlobalProperties.GetIPGlobalProperties().HostName.Trim(); } } From 5a0ae3f252dc8bbe0450e028ce242812f7562168 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Tue, 26 Nov 2019 16:22:33 +0100 Subject: [PATCH 5/9] Revert to case-insensitive comparison of hostnames in SmtpClient tests --- .../System.Net.Mail/tests/Functional/MockSmtpServer.cs | 2 +- .../System.Net.Mail/tests/Functional/SmtpClientTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs index 9fd82995bbefe..e00e84ba37ed3 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs @@ -105,7 +105,7 @@ async ValueTask SendMessageAsync(string text) string message = await ReceiveMessageAsync(); Debug.Assert(message.ToLower().StartsWith("helo ") || message.ToLower().StartsWith("ehlo ")); - ClientDomain = message.Substring(5); + ClientDomain = message.Substring(5).ToLower(); OnCommandReceived?.Invoke(message.Substring(0, 4), ClientDomain); OnHelloReceived?.Invoke(ClientDomain); diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 0cbcb28856987..3e582ef55300d 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -485,6 +485,6 @@ public async Task SendMailAsync_CanBeCanceled_CancellationToken() Assert.Equal(GetClientDomain(), server.ClientDomain); } - private static string GetClientDomain() => IPGlobalProperties.GetIPGlobalProperties().HostName.Trim(); + private static string GetClientDomain() => IPGlobalProperties.GetIPGlobalProperties().HostName.Trim().ToLower(); } } From 9375146d58dbea6387d47e859195bbb3c6b5cec5 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Tue, 26 Nov 2019 16:27:34 +0100 Subject: [PATCH 6/9] Disable SmtpClient NTLM test on Unix --- .../System.Net.Mail/tests/Functional/SmtpClientTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 3e582ef55300d..996d980255dc3 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -359,6 +359,8 @@ public async Task TestMailDeliveryAsync(string body) } [Fact] + [ActiveIssue(28961)] + [PlatformSpecific(TestPlatforms.Windows)] // NTLM support required public async Task TestCredentialsCopyInAsyncContext() { using var server = new MockSmtpServer(); From 4b8ef513e9e5d128777934f890c3e70b1c6fd11a Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Tue, 26 Nov 2019 18:48:43 +0100 Subject: [PATCH 7/9] Address PR feedback --- .../tests/Functional/MockSmtpServer.cs | 19 ------------------- .../tests/Functional/SmtpClientTest.cs | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs index e00e84ba37ed3..18b7899e6128b 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs @@ -128,25 +128,6 @@ async ValueTask SendMessageAsync(string text) AuthMethodUsed = parts[1]; - // PLAIN is not supported by SmtpClient - /* - if (parts[1].Equals("PLAIN", StringComparison.OrdinalIgnoreCase)) - { - string base64; - if (parts.Length == 2) - { - await SendMessageAsync("334"); - base64 = await ReceiveMessageAsync(); - } - else - { - base64 = parts[2]; - } - UsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); - await SendMessageAsync("235 Authentication successful"); - } - else - */ if (parts[1].Equals("LOGIN", StringComparison.OrdinalIgnoreCase)) { if (parts.Length == 2) diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 996d980255dc3..685adc4087c52 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -372,7 +372,7 @@ public async Task TestCredentialsCopyInAsyncContext() client.Credentials = cache; - // The mock server doesn't actually understand NTML, but still advertises support for it + // The mock server doesn't actually understand NTLM, but still advertises support for it server.AdvertiseNtlmAuthSupport = true; await Assert.ThrowsAsync(async () => await client.SendMailAsync(msg)); From 248cbfdfc169216a91ac02ebafaec18a9ceac2a3 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Wed, 27 Nov 2019 00:30:59 +0100 Subject: [PATCH 8/9] Address PR feedback --- ...ockSmtpServer.cs => LoopbackSmtpServer.cs} | 4 +-- .../tests/Functional/SmtpClientTest.cs | 28 ++++++++----------- .../System.Net.Mail.Functional.Tests.csproj | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) rename src/libraries/System.Net.Mail/tests/Functional/{MockSmtpServer.cs => LoopbackSmtpServer.cs} (99%) diff --git a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs similarity index 99% rename from src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs rename to src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs index 18b7899e6128b..4c83223faa3b1 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/MockSmtpServer.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs @@ -11,7 +11,7 @@ namespace Systen.Net.Mail.Tests { - public class MockSmtpServer : IDisposable + public class LoopbackSmtpServer : IDisposable { private static readonly ReadOnlyMemory MessageTerminator = new byte[] { (byte)'\r', (byte)'\n' }; private static readonly ReadOnlyMemory BodyTerminator = new byte[] { (byte)'\r', (byte)'\n', (byte)'.', (byte)'\r', (byte)'\n' }; @@ -46,7 +46,7 @@ public class MockSmtpServer : IDisposable public int ConnectionCount { get; private set; } public int MessagesReceived { get; private set; } - public MockSmtpServer() + public LoopbackSmtpServer() { _socketsToDispose = new ConcurrentBag(); _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 685adc4087c52..01558afbfac0d 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -291,12 +291,9 @@ public async Task SendAsync_ServerDoesntExist_Throws() [Fact] public void TestMailDelivery() { - const string Username = "Foo"; - const string Password = "Bar"; - - using var server = new MockSmtpServer(); + using var server = new LoopbackSmtpServer(); using SmtpClient client = server.CreateClient(); - client.Credentials = new NetworkCredential(Username, Password); + client.Credentials = new NetworkCredential("Foo", "Bar"); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); client.Send(msg); @@ -306,9 +303,9 @@ public void TestMailDelivery() Assert.Equal("hello", server.Message.Subject); Assert.Equal("howdydoo", server.Message.Body); Assert.Equal(GetClientDomain(), server.ClientDomain); - Assert.Equal(Username, server.Username); - Assert.Equal(Password, server.Password); - Assert.Equal("login", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); + Assert.Equal("Foo", server.Username); + Assert.Equal("Bar", server.Password); + Assert.Equal("LOGIN", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); } [Fact] @@ -345,7 +342,7 @@ public void TestZeroTimeout() [InlineData(null)] public async Task TestMailDeliveryAsync(string body) { - using var server = new MockSmtpServer(); + using var server = new LoopbackSmtpServer(); using SmtpClient client = server.CreateClient(); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", body); @@ -359,11 +356,10 @@ public async Task TestMailDeliveryAsync(string body) } [Fact] - [ActiveIssue(28961)] - [PlatformSpecific(TestPlatforms.Windows)] // NTLM support required + [PlatformSpecific(TestPlatforms.Windows)] // NTLM support required, see https://github.com/dotnet/corefx/issues/28961 public async Task TestCredentialsCopyInAsyncContext() { - using var server = new MockSmtpServer(); + using var server = new LoopbackSmtpServer(); using SmtpClient client = server.CreateClient(); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); @@ -376,7 +372,7 @@ public async Task TestCredentialsCopyInAsyncContext() server.AdvertiseNtlmAuthSupport = true; await Assert.ThrowsAsync(async () => await client.SendMailAsync(msg)); - Assert.Equal("ntlm", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); + Assert.Equal("NTLM", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); } @@ -397,7 +393,7 @@ public void SendMail_DeliveryFormat_SubjectEncoded(bool useAsyncSend, bool useSe // If the server does not support `SMTPUTF8` or use `SmtpDeliveryFormat.SevenBit`, the server should received this subject. const string subjectBase64 = "=?utf-8?B?VGVzdCDmtYvor5UgQ29udGFpbiDljIXlkKsgVVRGOA==?="; - using var server = new MockSmtpServer(); + using var server = new LoopbackSmtpServer(); using SmtpClient client = server.CreateClient(); // Setting up Server Support for `SMTPUTF8`. @@ -439,7 +435,7 @@ public void SendMail_DeliveryFormat_SubjectEncoded(bool useAsyncSend, bool useSe [Fact] public void SendMailAsync_CanBeCanceled_CancellationToken_SetAlready() { - using var server = new MockSmtpServer(); + using var server = new LoopbackSmtpServer(); using SmtpClient client = server.CreateClient(); CancellationTokenSource cts = new CancellationTokenSource(); @@ -456,7 +452,7 @@ public void SendMailAsync_CanBeCanceled_CancellationToken_SetAlready() [Fact] public async Task SendMailAsync_CanBeCanceled_CancellationToken() { - using var server = new MockSmtpServer(); + using var server = new LoopbackSmtpServer(); using SmtpClient client = server.CreateClient(); server.ReceiveMultipleConnections = true; diff --git a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj index d9058df715bf5..f91fe3a0abbb5 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj +++ b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj @@ -20,7 +20,7 @@ - + Common\System\Diagnostics\Tracing\TestEventListener.cs From 499a07e31c805179dd1e48ec904ab3f1e6485c17 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Wed, 27 Nov 2019 12:02:13 +0100 Subject: [PATCH 9/9] Address PR feedback Use Interlocked.Exchange instead of locks --- .../src/System/Net/Mail/SmtpClient.cs | 70 +++++++++---------- .../tests/Functional/LoopbackSmtpServer.cs | 6 +- 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs index 3ddd9b283e7fc..e81a56c61f04e 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs @@ -808,13 +808,37 @@ public Task SendMailAsync(MailMessage message, CancellationToken cancellationTok } // Create a TaskCompletionSource to represent the operation - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tcs = new TaskCompletionSource(); CancellationTokenRegistration ctr = default; + // Indicates whether the CTR has been set - captured in handler + int state = 0; + // Register a handler that will transfer completion results to the TCS Task SendCompletedEventHandler handler = null; - handler = (sender, e) => HandleCompletion(tcs, ctr, e, handler); + handler = (sender, e) => + { + if (e.UserState == tcs) + { + try + { + ((SmtpClient)sender).SendCompleted -= handler; + if (Interlocked.Exchange(ref state, 1) != 0) + { + // A CTR has been set, we have to wait until it completes before completing the task + ctr.Dispose(); + } + } + catch (ObjectDisposedException) { } // SendAsyncCancel will throw if SmtpClient was disposed + finally + { + if (e.Error != null) tcs.TrySetException(e.Error); + else if (e.Cancelled) tcs.TrySetCanceled(); + else tcs.TrySetResult(null); + } + } + }; SendCompleted += handler; // Start the async operation. @@ -828,49 +852,21 @@ public Task SendMailAsync(MailMessage message, CancellationToken cancellationTok throw; } - // Only register on the CT if HandleCompletion hasn't started to ensure the CTR is disposed - bool lockTaken = false; - try + ctr = cancellationToken.Register(s => { - Monitor.TryEnter(tcs, ref lockTaken); - if (lockTaken && !tcs.Task.IsCompleted) - { - ctr = cancellationToken.Register(s => - { - ((SmtpClient)s).SendAsyncCancel(); - }, this); - } - } - finally + ((SmtpClient)s).SendAsyncCancel(); + }, this); + + if (Interlocked.Exchange(ref state, 1) != 0) { - if (lockTaken) Monitor.Exit(tcs); + // SendCompleted was already invoked, ensure the CTR completes before returning the task + ctr.Dispose(); } // Return the task to represent the asynchronous operation return tcs.Task; } - private void HandleCompletion(TaskCompletionSource tcs, CancellationTokenRegistration ctr, AsyncCompletedEventArgs e, SendCompletedEventHandler handler) - { - if (e.UserState == tcs) - { - lock (tcs) - { - try - { - SendCompleted -= handler; - ctr.Dispose(); - } - finally - { - if (e.Error != null) tcs.TrySetException(e.Error); - else if (e.Cancelled) tcs.TrySetCanceled(); - else tcs.TrySetResult(null); - } - } - } - } - //********************************* // private methods diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs index 4c83223faa3b1..8db9e8c5e108a 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs @@ -13,8 +13,8 @@ namespace Systen.Net.Mail.Tests { public class LoopbackSmtpServer : IDisposable { - private static readonly ReadOnlyMemory MessageTerminator = new byte[] { (byte)'\r', (byte)'\n' }; - private static readonly ReadOnlyMemory BodyTerminator = new byte[] { (byte)'\r', (byte)'\n', (byte)'.', (byte)'\r', (byte)'\n' }; + private static readonly ReadOnlyMemory s_messageTerminator = new byte[] { (byte)'\r', (byte)'\n' }; + private static readonly ReadOnlyMemory s_bodyTerminator = new byte[] { (byte)'\r', (byte)'\n', (byte)'.', (byte)'\r', (byte)'\n' }; public bool ReceiveMultipleConnections = false; public bool SupportSmtpUTF8 = false; @@ -75,7 +75,7 @@ private async Task HandleConnectionAsync(Socket socket) async ValueTask ReceiveMessageAsync(bool isBody = false) { - var terminator = isBody ? BodyTerminator : MessageTerminator; + var terminator = isBody ? s_bodyTerminator : s_messageTerminator; int suffix = terminator.Length; int received = 0;