SmtpClient SendMailAsync with CancellationToken API implementation (#287)
authorMiha Zupan <mihazupan.zupan1@gmail.com>
Wed, 27 Nov 2019 14:49:43 +0000 (15:49 +0100)
committerGitHub <noreply@github.com>
Wed, 27 Nov 2019 14:49:43 +0000 (15:49 +0100)
* Add SmtpClient.SendMailAsync overloads with cancellation

* Rework mock SmtpClient tests

* Verify that SmtpClient uses Auth if available

* Add tests for SmtpClient.SendMailAsync using CancellationTokens

* Revert to case-insensitive comparison of hostnames in SmtpClient tests

* Disable SmtpClient NTLM test on Unix

* Address PR feedback

* Address PR feedback

* Address PR feedback

Use Interlocked.Exchange instead of locks

src/libraries/System.Net.Mail/ref/System.Net.Mail.cs
src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs
src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs [new file with mode: 0644]
src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs
src/libraries/System.Net.Mail/tests/Functional/SmtpServer.cs [deleted file]
src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj

index d1b280aec9489a7aafb93a77824cb26b8218590c..b3c1f32996dca9dd3a44e980044734491444bc1a 100644 (file)
@@ -186,6 +186,8 @@ namespace System.Net.Mail
         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
     {
index 01bb0a44e73558b4529f03700dc23d457286c6d8..077bfeb57175f2ad2da21b5730a5c148b580631a 100644 (file)
@@ -782,17 +782,59 @@ namespace System.Net.Mail
         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<object>();
 
+            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, 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.
@@ -806,22 +848,19 @@ namespace System.Net.Mail
                 throw;
             }
 
-            // Return the task to represent the asynchronous operation
-            return tcs.Task;
-        }
+            ctr = cancellationToken.Register(s =>
+            {
+                ((SmtpClient)s).SendAsyncCancel();
+            }, this);
 
-        private void HandleCompletion(TaskCompletionSource<object> tcs, AsyncCompletedEventArgs e, SendCompletedEventHandler handler)
-        {
-            if (e.UserState == tcs)
+            if (Interlocked.Exchange(ref state, 1) != 0)
             {
-                try { SendCompleted -= handler; }
-                finally
-                {
-                    if (e.Error != null) tcs.TrySetException(e.Error);
-                    else if (e.Cancelled) tcs.TrySetCanceled();
-                    else tcs.TrySetResult(null);
-                }
+                // 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;
         }
 
 
diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs
new file mode 100644 (file)
index 0000000..8db9e8c
--- /dev/null
@@ -0,0 +1,265 @@
+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 LoopbackSmtpServer : IDisposable
+    {
+        private static readonly ReadOnlyMemory<byte> s_messageTerminator = new byte[] { (byte)'\r', (byte)'\n' };
+        private static readonly ReadOnlyMemory<byte> s_bodyTerminator = new byte[] { (byte)'\r', (byte)'\n', (byte)'.', (byte)'\r', (byte)'\n' };
+
+        public bool ReceiveMultipleConnections = false;
+        public bool SupportSmtpUTF8 = false;
+        public bool AdvertiseNtlmAuthSupport = false;
+
+        private bool _disposed = false;
+        private readonly Socket _listenSocket;
+        private readonly ConcurrentBag<Socket> _socketsToDispose;
+        private long _messageCounter = new Random().Next(1000, 2000);
+
+        public readonly int Port;
+        public SmtpClient CreateClient() => new SmtpClient("localhost", Port);
+
+        public Action<Socket> OnConnected;
+        public Action<string> OnHelloReceived;
+        public Action<string, string> OnCommandReceived;
+        public Action<string> OnUnknownCommand;
+        public Action<Socket> 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 LoopbackSmtpServer()
+        {
+            _socketsToDispose = new ConcurrentBag<Socket>();
+            _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<string> ReceiveMessageAsync(bool isBody = false)
+            {
+                var terminator = isBody ? s_bodyTerminator : s_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).ToLower();
+                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" + (AdvertiseNtlmAuthSupport ? " NTLM" : ""));
+
+                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];
+
+                        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 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;
+                    }
+
+                    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 <CRLF>.<CRLF>");
+                            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<string, string> 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<string, string> headers, string body)
+            {
+                Headers = headers;
+                Body = body;
+            }
+
+            public static ParsedMailMessage Parse(string data)
+            {
+                Dictionary<string, string> headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+                ReadOnlySpan<char> 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);
+            }
+        }
+    }
+}
index 2c8bfdfe73dc82e88b1ce2df808f8feeffdce532..01558afbfac0d7ef0336eec15472bedb03748680 100644 (file)
@@ -14,6 +14,7 @@ using System.Net.NetworkInformation;
 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,21 @@ namespace System.Net.Mail.Tests
         [Fact]
         public void TestMailDelivery()
         {
-            SmtpServer server = new SmtpServer();
-            SmtpClient client = new SmtpClient("localhost", server.EndPoint.Port);
-            client.Credentials = new NetworkCredential("user", "password");
+            using var server = new LoopbackSmtpServer();
+            using SmtpClient client = server.CreateClient();
+            client.Credentials = new NetworkCredential("Foo", "Bar");
             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("<foo@example.com>", server.MailFrom);
-                Assert.Equal("<bar@example.com>", server.MailTo);
-                Assert.Equal("hello", server.Subject);
-                Assert.Equal("howdydoo", server.Body);
-                Assert.Equal(clientDomain, server.ClientDomain);
-            }
-            finally
-            {
-                server.Stop();
-            }
+            Assert.Equal("<foo@example.com>", server.MailFrom);
+            Assert.Equal("<bar@example.com>", server.MailTo);
+            Assert.Equal("hello", server.Message.Subject);
+            Assert.Equal("howdydoo", server.Message.Body);
+            Assert.Equal(GetClientDomain(), server.ClientDomain);
+            Assert.Equal("Foo", server.Username);
+            Assert.Equal("Bar", server.Password);
+            Assert.Equal("LOGIN", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase);
         }
 
         [Fact]
@@ -349,60 +342,37 @@ namespace System.Net.Mail.Tests
         [InlineData(null)]
         public async Task TestMailDeliveryAsync(string body)
         {
-            SmtpServer server = new SmtpServer();
-            SmtpClient client = new SmtpClient("localhost", server.EndPoint.Port);
+            using var server = new LoopbackSmtpServer();
+            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("<foo@example.com>", server.MailFrom);
-                Assert.Equal("<bar@example.com>", 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("<foo@example.com>", server.MailFrom);
+            Assert.Equal("<bar@example.com>", server.MailTo);
+            Assert.Equal("hello", server.Message.Subject);
+            Assert.Equal(body ?? "", server.Message.Body);
+            Assert.Equal(GetClientDomain(), server.ClientDomain);
         }
 
         [Fact]
+        [PlatformSpecific(TestPlatforms.Windows)] // NTLM support required, see https://github.com/dotnet/corefx/issues/28961
         public async Task TestCredentialsCopyInAsyncContext()
         {
-            SmtpServer server = new SmtpServer();
-            SmtpClient client = new SmtpClient("localhost", server.EndPoint.Port);
+            using var server = new LoopbackSmtpServer();
+            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("<foo@example.com>", server.MailFrom);
-                Assert.Equal("<bar@example.com>", server.MailTo);
-                Assert.Equal("hello", server.Subject);
-                Assert.Equal("howdydoo", server.Body);
-                Assert.Equal(clientDomain, server.ClientDomain);
-            }
-            finally
-            {
-                server.Stop();
-            }
+            // The mock server doesn't actually understand NTLM, but still advertises support for it
+            server.AdvertiseNtlmAuthSupport = true;
+            await Assert.ThrowsAsync<SmtpException>(async () => await client.SendMailAsync(msg));
+
+            Assert.Equal("NTLM", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase);
         }
 
 
@@ -423,13 +393,12 @@ namespace System.Net.Mail.Tests
             // 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 LoopbackSmtpServer();
+            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 +413,76 @@ namespace System.Net.Mail.Tests
             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);
             }
         }
+
+        [Fact]
+        public void SendMailAsync_CanBeCanceled_CancellationToken_SetAlready()
+        {
+            using var server = new LoopbackSmtpServer();
+            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 LoopbackSmtpServer();
+            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<TaskCanceledException>(async () => await sendTask);
+
+            // We should still be able to send mail on the SmtpClient instance
+            await client.SendMailAsync(message);
+
+            Assert.Equal("<foo@internet.com>", server.MailFrom);
+            Assert.Equal("<bar@internet.com>", 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().ToLower();
     }
 }
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 (file)
index 403f7c5..0000000
+++ /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 <harinath@hurrynot.org>
-//
-
-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);
-        }
-    }
-}
index 9bdd31a46ace3f4c202b24358a042f690d1d377d..f91fe3a0abbb5c45a7ab5af058c4d732dae2601c 100644 (file)
@@ -20,7 +20,7 @@
     <Compile Include="MailMessageTest.cs" />
     <Compile Include="SmtpClientTest.cs" />
     <Compile Include="SmtpExceptionTest.cs" />
-    <Compile Include="SmtpServer.cs" />
+    <Compile Include="LoopbackSmtpServer.cs" />
     <Compile Include="$(CommonTestPath)System\Diagnostics\Tracing\TestEventListener.cs">
       <Link>Common\System\Diagnostics\Tracing\TestEventListener.cs</Link>
     </Compile>