From 631e734dfcda1e0d6c52d98aa0b4fe300e190bcc Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 7 May 2025 13:43:09 +0200 Subject: [PATCH 1/5] Test improvements --- .../tests/Functional/LoggingTest.cs | 2 +- .../tests/Functional/LoopbackServerTestBase.cs | 6 ++++-- .../tests/Functional/LoopbackSmtpServer.cs | 5 +++-- .../tests/Functional/MailMessageTest.cs | 18 ++++++++++++------ .../Functional/SmtpClientConnectionTest.cs | 11 +++++++++++ .../tests/Functional/SmtpClientTest.cs | 14 +++++++++++--- 6 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoggingTest.cs b/src/libraries/System.Net.Mail/tests/Functional/LoggingTest.cs index 188dd1711f52f8..31225bbb60ba5b 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/LoggingTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoggingTest.cs @@ -36,7 +36,7 @@ await RemoteExecutor.Invoke(() => listener.RunWithCallback(events.Enqueue, () => { // Invoke a test that'll cause some events to be generated - new SmtpClientTest().TestMailDelivery(); + new SmtpClientTest(null!).TestMailDelivery(); }); Assert.DoesNotContain(events, ev => ev.EventId == 0); // errors from the EventSource itself Assert.InRange(events.Count, 1, int.MaxValue); diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs index 0d00087e61427f..7c3c170de145a1 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Net.NetworkInformation; using System.Net.Security; using System.Security.Authentication; @@ -45,6 +46,7 @@ public struct SendMailAsyncMethod : ISendMethodProvider public abstract class LoopbackServerTestBase : IDisposable where T : ISendMethodProvider { + private static TimeSpan s_PassingTestTimeout = Debugger.IsAttached ? TimeSpan.FromSeconds(10000) : TimeSpan.FromSeconds(30); protected LoopbackSmtpServer Server { get; private set; } protected ITestOutputHelper Output { get; private set; } @@ -152,13 +154,13 @@ public LoopbackServerTestBase(ITestOutputHelper output) protected async Task SendMail(MailMessage msg, CancellationToken cancellationToken = default) { - Exception? ex = await SendMailInternal(msg, cancellationToken, null); + Exception? ex = await SendMailInternal(msg, cancellationToken, null).WaitAsync(s_PassingTestTimeout); Assert.Null(ex); } protected async Task SendMail(MailMessage msg, CancellationToken cancellationToken = default, bool unwrapException = true, bool asyncDirectException = false) where TException : Exception { - Exception? ex = await SendMailInternal(msg, cancellationToken, asyncDirectException); + Exception? ex = await SendMailInternal(msg, cancellationToken, asyncDirectException).WaitAsync(s_PassingTestTimeout); if (unwrapException && T.SendMethod != SendMethod.Send && typeof(TException) != typeof(SmtpException)) { diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs index 29d9a978890119..5cd541638b23b0 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs @@ -63,10 +63,11 @@ public LoopbackSmtpServer(ITestOutputHelper? output = null) { _output = output; _socketsToDispose = new ConcurrentBag(); - _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); _socketsToDispose.Add(_listenSocket); - _listenSocket.Bind(new IPEndPoint(IPAddress.Any, 0)); + // if dual socket supported, bind to Ipv6Any, otherwise Any + _listenSocket.Bind(new IPEndPoint(_listenSocket.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any, 0)); Port = ((IPEndPoint)_listenSocket.LocalEndPoint).Port; _listenSocket.Listen(1); diff --git a/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs b/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs index c73daa15422856..757383276777e1 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs @@ -11,8 +11,10 @@ using System.IO; using System.Reflection; +using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using Xunit; namespace System.Net.Mail.Tests @@ -259,13 +261,17 @@ private static (string Raw, string Attachment) DecodeSentMailMessage(MailMessage culture: null, activationAttributes: null); + // var syncSendAdapterType = typeof(MailMessage).Assembly.GetTypes() + // .FirstOrDefault(t => t.Name == "SyncReadWriteAdapter"); + // Send the message. - typeof(MailMessage).InvokeMember( - name: "Send", - invokeAttr: BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod, - binder: null, - target: mail, - args: new object[] { mailWriter, true, true }); + // typeof(MailMessage) + // .GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic) + // .MakeGenericMethod(syncSendAdapterType) + // .Invoke(mail, new object[] { mailWriter, true, true, CancellationToken.None }); + typeof(MailMessage) + .GetMethod("Send", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(mail, new object[] { mailWriter, true, true }); // Decode contents. string result = Encoding.UTF8.GetString(stream.ToArray()); diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs index 4ae8ccf4d692c8..7c52ba6d72cebc 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs @@ -22,6 +22,17 @@ public async Task SocketClosed() await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); } + [Fact] + public async Task UnrecognizedReply_Throws() + { + Server.OnCommandReceived = (command, arg) => + { + return "Go away"; + }; + + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + } + [Fact] public async Task EHelloNotRecognized_RestartWithHello() { diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 332b14b3baed78..a3b25b27d790a9 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -20,6 +20,7 @@ using Microsoft.DotNet.RemoteExecutor; using System.Net.Test.Common; using Xunit; +using Xunit.Abstractions; namespace System.Net.Mail.Tests { @@ -53,6 +54,13 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + ITestOutputHelper _output; + + public SmtpClientTest(ITestOutputHelper output) + { + _output = output; + } + [Theory] [InlineData(SmtpDeliveryMethod.SpecifiedPickupDirectory)] [InlineData(SmtpDeliveryMethod.PickupDirectoryFromIis)] @@ -237,7 +245,7 @@ public async Task SendAsync_ServerDoesntExist_Throws() [Fact] public void TestMailDelivery() { - using var server = new LoopbackSmtpServer(); + using var server = new LoopbackSmtpServer(_output); using SmtpClient client = server.CreateClient(); client.Credentials = new NetworkCredential("foo", "bar"); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); @@ -282,7 +290,7 @@ public void TestZeroTimeout() [Fact] public void SendMailAsync_CanBeCanceled_CancellationToken_SetAlready() { - using var server = new LoopbackSmtpServer(); + using var server = new LoopbackSmtpServer(_output); using SmtpClient client = server.CreateClient(); CancellationTokenSource cts = new CancellationTokenSource(); @@ -299,7 +307,7 @@ public void SendMailAsync_CanBeCanceled_CancellationToken_SetAlready() [Fact] public async Task SendMailAsync_CanBeCanceled_CancellationToken() { - using var server = new LoopbackSmtpServer(); + using var server = new LoopbackSmtpServer(_output); using SmtpClient client = server.CreateClient(); server.ReceiveMultipleConnections = true; From 8b56934549b82aa14278bd89aa638c02b97f67ff Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 7 May 2025 15:03:18 +0200 Subject: [PATCH 2/5] SmtpCommands and SmtpReplyReader --- .../src/System.Net.Mail.csproj | 1 + .../src/System/Net/BufferBuilder.cs | 7 + .../src/System/Net/Mail/ReadWriteAdapter.cs | 61 ++ .../src/System/Net/Mail/SmtpCommands.cs | 548 ++++++++---------- .../src/System/Net/Mail/SmtpConnection.cs | 18 +- .../src/System/Net/Mail/SmtpReplyReader.cs | 64 +- .../System/Net/Mail/SmtpReplyReaderFactory.cs | 215 ++----- .../src/System/Net/Mail/SmtpTransport.cs | 2 +- .../Unit/System.Net.Mail.Unit.Tests.csproj | 2 + 9 files changed, 431 insertions(+), 487 deletions(-) create mode 100644 src/libraries/System.Net.Mail/src/System/Net/Mail/ReadWriteAdapter.cs diff --git a/src/libraries/System.Net.Mail/src/System.Net.Mail.csproj b/src/libraries/System.Net.Mail/src/System.Net.Mail.csproj index 004fbbadd9ac77..189adf6b6c18e3 100644 --- a/src/libraries/System.Net.Mail/src/System.Net.Mail.csproj +++ b/src/libraries/System.Net.Mail/src/System.Net.Mail.csproj @@ -47,6 +47,7 @@ + diff --git a/src/libraries/System.Net.Mail/src/System/Net/BufferBuilder.cs b/src/libraries/System.Net.Mail/src/System/Net/BufferBuilder.cs index 2ca6684a405eaf..bbb1afb25fbb28 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/BufferBuilder.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/BufferBuilder.cs @@ -34,6 +34,13 @@ internal void Append(byte value) _buffer[_offset++] = value; } + internal void Append(ReadOnlyMemory value) + { + EnsureBuffer(value.Length); + value.Span.CopyTo(_buffer.AsSpan(_offset)); + _offset += value.Length; + } + internal void Append(ReadOnlySpan value) { EnsureBuffer(value.Length); diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/ReadWriteAdapter.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/ReadWriteAdapter.cs new file mode 100644 index 00000000000000..a5fbdb6469d0f2 --- /dev/null +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/ReadWriteAdapter.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Mail +{ + internal interface IReadWriteAdapter + { + static abstract ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); + static abstract ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken); + static abstract ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken); + static abstract Task FlushAsync(Stream stream, CancellationToken cancellationToken); + static abstract Task WaitAsync(TaskCompletionSource waiter); + } + + internal readonly struct AsyncReadWriteAdapter : IReadWriteAdapter + { + public static ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => + stream.ReadAsync(buffer, cancellationToken); + + public static ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) => + stream.ReadAtLeastAsync(buffer, minimumBytes, throwOnEndOfStream, cancellationToken); + + public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken) => + stream.WriteAsync(buffer, cancellationToken); + + public static Task FlushAsync(Stream stream, CancellationToken cancellationToken) => stream.FlushAsync(cancellationToken); + + public static Task WaitAsync(TaskCompletionSource waiter) => waiter.Task; + } + + internal readonly struct SyncReadWriteAdapter : IReadWriteAdapter + { + public static ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => + new ValueTask(stream.Read(buffer.Span)); + + public static ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) => + new ValueTask(stream.ReadAtLeast(buffer.Span, minimumBytes, throwOnEndOfStream)); + + public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + stream.Write(buffer.Span); + return default; + } + + public static Task FlushAsync(Stream stream, CancellationToken cancellationToken) + { + stream.Flush(); + return Task.CompletedTask; + } + + public static Task WaitAsync(TaskCompletionSource waiter) + { + waiter.Task.GetAwaiter().GetResult(); + return Task.CompletedTask; + } + } +} diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs index 60bab14537f706..e8b5cae5cfe960 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs @@ -2,204 +2,117 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.Collections; using System.Globalization; using System.IO; using System.Net.Mime; using System.Runtime.ExceptionServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mail { internal static class CheckCommand { - private static readonly AsyncCallback s_onReadLine = new AsyncCallback(OnReadLine); - private static readonly AsyncCallback s_onWrite = new AsyncCallback(OnWrite); - - internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback? callback, object? state) + internal static LineInfo Send(SmtpConnection conn) { - MultiAsyncResult multiResult = new MultiAsyncResult(conn, callback, state); - multiResult.Enter(); - IAsyncResult writeResult = conn.BeginFlush(s_onWrite, multiResult); - if (writeResult.CompletedSynchronously) - { - conn.EndFlush(writeResult); - multiResult.Leave(); - } - SmtpReplyReader reader = conn.Reader!.GetNextReplyReader(); - multiResult.Enter(); + Task task = SendAsync(conn); + Debug.Assert(task.IsCompleted, "CheckCommand.SendAsync should be completed synchronously."); + return task.GetAwaiter().GetResult(); + } - //this actually does a read on the stream. - IAsyncResult result = reader.BeginReadLine(s_onReadLine, multiResult); - if (result.CompletedSynchronously) - { - LineInfo info = SmtpReplyReader.EndReadLine(result); - if (!(multiResult.Result is Exception)) - multiResult.Result = info; - multiResult.Leave(); - } - multiResult.CompleteSequence(); - return multiResult; + internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback callback, object? state) + { + return TaskToAsyncResult.Begin(SendAsync(conn), callback, state); } + internal static LineInfo EndSend(IAsyncResult asyncResult) + { + return TaskToAsyncResult.End(asyncResult); + } - internal static object EndSend(IAsyncResult result, out string response) + internal static async Task SendAsync(SmtpConnection conn, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { - object commandResult = MultiAsyncResult.End(result)!; - if (commandResult is Exception e) - { - ExceptionDispatchInfo.Throw(e); - } + await conn.FlushAsync(cancellationToken).ConfigureAwait(false); + return await conn.Reader!.GetNextReplyReader().ReadLineAsync(cancellationToken).ConfigureAwait(false); + } + } - LineInfo info = (LineInfo)commandResult; - response = info.Line; - return info.StatusCode; + internal static class ReadLinesCommand + { + internal static LineInfo[] Send(SmtpConnection conn) + { + Task task = SendAsync(conn); + Debug.Assert(task.IsCompleted, "ReadLinesCommand.SendAsync should be completed synchronously."); + return task.GetAwaiter().GetResult(); } - private static void OnReadLine(IAsyncResult result) + internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback callback, object? state) { - if (!result.CompletedSynchronously) - { - MultiAsyncResult multiResult = (MultiAsyncResult)result.AsyncState!; - try - { - SmtpConnection conn = (SmtpConnection)multiResult.Context; - LineInfo info = SmtpReplyReader.EndReadLine(result); - if (!(multiResult.Result is Exception)) - multiResult.Result = info; - multiResult.Leave(); - } - catch (Exception e) - { - multiResult.Leave(e); - } - } + return TaskToAsyncResult.Begin(SendAsync(conn), callback, state); } - private static void OnWrite(IAsyncResult result) + internal static LineInfo[] EndSend(IAsyncResult asyncResult) { - if (!result.CompletedSynchronously) - { - MultiAsyncResult multiResult = (MultiAsyncResult)result.AsyncState!; - try - { - SmtpConnection conn = (SmtpConnection)multiResult.Context; - conn.EndFlush(result); - multiResult.Leave(); - } - catch (Exception e) - { - multiResult.Leave(e); - } - } + return TaskToAsyncResult.End(asyncResult); } - internal static SmtpStatusCode Send(SmtpConnection conn, out string response) + internal static async Task SendAsync(SmtpConnection conn, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { - conn.Flush(); - SmtpReplyReader reader = conn.Reader!.GetNextReplyReader(); - LineInfo info = reader.ReadLine(); - response = info.Line; - reader.Close(); - return info.StatusCode; + await conn.FlushAsync(cancellationToken).ConfigureAwait(false); + return await conn.Reader!.GetNextReplyReader().ReadLinesAsync(cancellationToken).ConfigureAwait(false); } } - internal static class ReadLinesCommand + internal static class AuthCommand { - private static readonly AsyncCallback s_onReadLines = new AsyncCallback(OnReadLines); - private static readonly AsyncCallback s_onWrite = new AsyncCallback(OnWrite); - - internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback? callback, object? state) + internal static LineInfo Send(SmtpConnection conn, string type, string message) { - MultiAsyncResult multiResult = new MultiAsyncResult(conn, callback, state); - multiResult.Enter(); - IAsyncResult writeResult = conn.BeginFlush(s_onWrite, multiResult); - if (writeResult.CompletedSynchronously) - { - conn.EndFlush(writeResult); - multiResult.Leave(); - } - SmtpReplyReader reader = conn.Reader!.GetNextReplyReader(); - multiResult.Enter(); - IAsyncResult readLinesResult = reader.BeginReadLines(s_onReadLines, multiResult); - if (readLinesResult.CompletedSynchronously) - { - LineInfo[] lines = SmtpReplyReader.EndReadLines(readLinesResult); - if (!(multiResult.Result is Exception)) - multiResult.Result = lines; - multiResult.Leave(); - } - multiResult.CompleteSequence(); - return multiResult; + Task task = SendAsync(conn, type, message); + Debug.Assert(task.IsCompleted, "AuthCommand.SendAsync should be completed synchronously."); + return task.GetAwaiter().GetResult(); } - internal static LineInfo[] EndSend(IAsyncResult result) + internal static LineInfo Send(SmtpConnection conn, string? message) { - object commandResult = MultiAsyncResult.End(result)!; - if (commandResult is Exception e) - { - ExceptionDispatchInfo.Throw(e); - } - return (LineInfo[])commandResult; + Task task = SendAsync(conn, message); + Debug.Assert(task.IsCompleted, "AuthCommand.SendAsync should be completed synchronously."); + return task.GetAwaiter().GetResult(); } - private static void OnReadLines(IAsyncResult result) + internal static IAsyncResult BeginSend(SmtpConnection conn, string type, string message, AsyncCallback callback, object? state) { - if (!result.CompletedSynchronously) - { - MultiAsyncResult multiResult = (MultiAsyncResult)result.AsyncState!; - try - { - SmtpConnection conn = (SmtpConnection)multiResult.Context; - LineInfo[] lines = SmtpReplyReader.EndReadLines(result); - if (!(multiResult.Result is Exception)) - multiResult.Result = lines; - multiResult.Leave(); - } - catch (Exception e) - { - multiResult.Leave(e); - } - } + return TaskToAsyncResult.Begin(SendAsync(conn, type, message), callback, state); } - private static void OnWrite(IAsyncResult result) + internal static IAsyncResult BeginSend(SmtpConnection conn, string? message, AsyncCallback callback, object? state) { - if (!result.CompletedSynchronously) - { - MultiAsyncResult multiResult = (MultiAsyncResult)result.AsyncState!; - try - { - SmtpConnection conn = (SmtpConnection)multiResult.Context; - conn.EndFlush(result); - multiResult.Leave(); - } - catch (Exception e) - { - multiResult.Leave(e); - } - } + return TaskToAsyncResult.Begin(SendAsync(conn, message), callback, state); } - internal static LineInfo[] Send(SmtpConnection conn) + + internal static LineInfo EndSend(IAsyncResult asyncResult) { - conn.Flush(); - return conn.Reader!.GetNextReplyReader().ReadLines(); + return TaskToAsyncResult.End(asyncResult); } - } - internal static class AuthCommand - { - internal static IAsyncResult BeginSend(SmtpConnection conn, string type, string message, AsyncCallback? callback, object? state) + internal static async Task SendAsync(SmtpConnection conn, string type, string message, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn, type, message); - return ReadLinesCommand.BeginSend(conn, callback, state); + LineInfo[] lines = await ReadLinesCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + return CheckResponse(lines); } - internal static IAsyncResult BeginSend(SmtpConnection conn, string? message, AsyncCallback? callback, object? state) + internal static async Task SendAsync(SmtpConnection conn, string? message, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn, message); - return ReadLinesCommand.BeginSend(conn, callback, state); + LineInfo[] lines = await ReadLinesCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + return CheckResponse(lines); } private static LineInfo CheckResponse(LineInfo[] lines) @@ -212,13 +125,9 @@ private static LineInfo CheckResponse(LineInfo[] lines) return lines[0]; } - internal static LineInfo EndSend(IAsyncResult result) - { - return CheckResponse(ReadLinesCommand.EndSend(result)); - } private static void PrepareCommand(SmtpConnection conn, string type, string message) { - conn.BufferBuilder.Append(SmtpCommands.Auth); + conn.BufferBuilder.Append(SmtpCommands.Auth.Span); conn.BufferBuilder.Append(type); conn.BufferBuilder.Append((byte)' '); conn.BufferBuilder.Append(message); @@ -230,26 +139,33 @@ private static void PrepareCommand(SmtpConnection conn, string? message) conn.BufferBuilder.Append(message); conn.BufferBuilder.Append(SmtpCommands.CRLF); } + } - internal static LineInfo Send(SmtpConnection conn, string type, string message) + internal static class DataCommand + { + internal static void Send(SmtpConnection conn) { - PrepareCommand(conn, type, message); - return CheckResponse(ReadLinesCommand.Send(conn)); + Task task = SendAsync(conn); + Debug.Assert(task.IsCompleted, "DataCommand.SendAsync should be completed synchronously."); + task.GetAwaiter().GetResult(); } - internal static LineInfo Send(SmtpConnection conn, string? message) + internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback callback, object? state) { - PrepareCommand(conn, message); - return CheckResponse(ReadLinesCommand.Send(conn)); + return TaskToAsyncResult.Begin(SendAsync(conn), callback, state); } - } - internal static class DataCommand - { - internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback? callback, object? state) + internal static void EndSend(IAsyncResult asyncResult) + { + TaskToAsyncResult.End(asyncResult); + } + + internal static async Task SendAsync(SmtpConnection conn, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn); - return CheckCommand.BeginSend(conn, callback, state); + LineInfo info = await CheckCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + CheckResponse(info.StatusCode, info.Line); } private static void CheckResponse(SmtpStatusCode statusCode, string serverResponse) @@ -274,13 +190,6 @@ private static void CheckResponse(SmtpStatusCode statusCode, string serverRespon } } - internal static void EndSend(IAsyncResult result) - { - string response; - SmtpStatusCode statusCode = (SmtpStatusCode)CheckCommand.EndSend(result, out response); - CheckResponse(statusCode, response); - } - private static void PrepareCommand(SmtpConnection conn) { if (conn.IsStreamOpen) @@ -290,18 +199,35 @@ private static void PrepareCommand(SmtpConnection conn) conn.BufferBuilder.Append(SmtpCommands.Data); } + } + internal static class DataStopCommand + { internal static void Send(SmtpConnection conn) + { + Task task = SendAsync(conn); + Debug.Assert(task.IsCompleted, "DataStopCommand.SendAsync should be completed synchronously."); + task.GetAwaiter().GetResult(); + } + + internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback callback, object? state) + { + return TaskToAsyncResult.Begin(SendAsync(conn), callback, state); + } + + internal static void EndSend(IAsyncResult asyncResult) + { + TaskToAsyncResult.End(asyncResult); + } + + internal static async Task SendAsync(SmtpConnection conn, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn); - string response; - SmtpStatusCode statusCode = CheckCommand.Send(conn, out response); - CheckResponse(statusCode, response); + LineInfo info = await CheckCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + CheckResponse(info.StatusCode, info.Line); } - } - internal static class DataStopCommand - { private static void CheckResponse(SmtpStatusCode statusCode, string serverResponse) { switch (statusCode) @@ -335,21 +261,33 @@ private static void PrepareCommand(SmtpConnection conn) conn.BufferBuilder.Append(SmtpCommands.DataStop); } - internal static void Send(SmtpConnection conn) - { - PrepareCommand(conn); - string response; - SmtpStatusCode statusCode = CheckCommand.Send(conn, out response); - CheckResponse(statusCode, response); - } } internal static class EHelloCommand { - internal static IAsyncResult BeginSend(SmtpConnection conn, string domain, AsyncCallback? callback, object? state) + internal static string[] Send(SmtpConnection conn, string domain) + { + Task task = SendAsync(conn, domain); + Debug.Assert(task.IsCompleted, "EHelloCommand.SendAsync should be completed synchronously."); + return task.GetAwaiter().GetResult(); + } + + internal static IAsyncResult BeginSend(SmtpConnection conn, string domain, AsyncCallback callback, object? state) + { + return TaskToAsyncResult.Begin(SendAsync(conn, domain), callback, state); + } + + internal static string[] EndSend(IAsyncResult asyncResult) + { + return TaskToAsyncResult.End(asyncResult); + } + + internal static async Task SendAsync(SmtpConnection conn, string domain, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn, domain); - return ReadLinesCommand.BeginSend(conn, callback, state); + LineInfo[] lines = await ReadLinesCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + return CheckResponse(lines); } private static string[] CheckResponse(LineInfo[] lines) @@ -375,10 +313,6 @@ private static string[] CheckResponse(LineInfo[] lines) return extensions; } - internal static string[] EndSend(IAsyncResult result) - { - return CheckResponse(ReadLinesCommand.EndSend(result)); - } private static void PrepareCommand(SmtpConnection conn, string domain) { if (conn.IsStreamOpen) @@ -390,20 +324,33 @@ private static void PrepareCommand(SmtpConnection conn, string domain) conn.BufferBuilder.Append(domain); conn.BufferBuilder.Append(SmtpCommands.CRLF); } - - internal static string[] Send(SmtpConnection conn, string domain) - { - PrepareCommand(conn, domain); - return CheckResponse(ReadLinesCommand.Send(conn)); - } } internal static class HelloCommand { - internal static IAsyncResult BeginSend(SmtpConnection conn, string domain, AsyncCallback? callback, object? state) + internal static void Send(SmtpConnection conn, string domain) + { + Task task = SendAsync(conn, domain); + Debug.Assert(task.IsCompleted, "HelloCommand.SendAsync should be completed synchronously."); + task.GetAwaiter().GetResult(); + } + + internal static IAsyncResult BeginSend(SmtpConnection conn, string domain, AsyncCallback callback, object? state) + { + return TaskToAsyncResult.Begin(SendAsync(conn, domain), callback, state); + } + + internal static void EndSend(IAsyncResult asyncResult) + { + TaskToAsyncResult.End(asyncResult); + } + + internal static async Task SendAsync(SmtpConnection conn, string domain, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn, domain); - return CheckCommand.BeginSend(conn, callback, state); + LineInfo info = await CheckCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + CheckResponse(info.StatusCode, info.Line); } private static void CheckResponse(SmtpStatusCode statusCode, string serverResponse) @@ -426,13 +373,6 @@ private static void CheckResponse(SmtpStatusCode statusCode, string serverRespon } } - internal static void EndSend(IAsyncResult result) - { - string response; - SmtpStatusCode statusCode = (SmtpStatusCode)CheckCommand.EndSend(result, out response); - CheckResponse(statusCode, response); - } - private static void PrepareCommand(SmtpConnection conn, string domain) { if (conn.IsStreamOpen) @@ -444,22 +384,33 @@ private static void PrepareCommand(SmtpConnection conn, string domain) conn.BufferBuilder.Append(domain); conn.BufferBuilder.Append(SmtpCommands.CRLF); } - - internal static void Send(SmtpConnection conn, string domain) - { - PrepareCommand(conn, domain); - string response; - SmtpStatusCode statusCode = CheckCommand.Send(conn, out response); - CheckResponse(statusCode, response); - } } internal static class StartTlsCommand { - internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback? callback, object? state) + internal static void Send(SmtpConnection conn) + { + Task task = SendAsync(conn); + Debug.Assert(task.IsCompleted, "StartTlsCommand.SendAsync should be completed synchronously."); + task.GetAwaiter().GetResult(); + } + + internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback callback, object? state) + { + return TaskToAsyncResult.Begin(SendAsync(conn), callback, state); + } + + internal static void EndSend(IAsyncResult asyncResult) + { + TaskToAsyncResult.End(asyncResult); + } + + internal static async Task SendAsync(SmtpConnection conn, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn); - return CheckCommand.BeginSend(conn, callback, state); + LineInfo info = await CheckCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + CheckResponse(info.StatusCode, info.Line); } private static void CheckResponse(SmtpStatusCode statusCode, string response) @@ -484,13 +435,6 @@ private static void CheckResponse(SmtpStatusCode statusCode, string response) } } - internal static void EndSend(IAsyncResult result) - { - string response; - SmtpStatusCode statusCode = (SmtpStatusCode)CheckCommand.EndSend(result, out response); - CheckResponse(statusCode, response); - } - private static void PrepareCommand(SmtpConnection conn) { if (conn.IsStreamOpen) @@ -501,23 +445,33 @@ private static void PrepareCommand(SmtpConnection conn) conn.BufferBuilder.Append(SmtpCommands.StartTls); conn.BufferBuilder.Append(SmtpCommands.CRLF); } - - internal static void Send(SmtpConnection conn) - { - PrepareCommand(conn); - string response; - SmtpStatusCode statusCode = CheckCommand.Send(conn, out response); - CheckResponse(statusCode, response); - } } internal static class MailCommand { - internal static IAsyncResult BeginSend(SmtpConnection conn, ReadOnlySpan command, MailAddress from, - bool allowUnicode, AsyncCallback? callback, object? state) + internal static void Send(SmtpConnection conn, ReadOnlyMemory command, MailAddress from, bool allowUnicode) + { + Task task = SendAsync(conn, command, from, allowUnicode); + Debug.Assert(task.IsCompleted, "MailCommand.SendAsync should be completed synchronously."); + task.GetAwaiter().GetResult(); + } + + internal static IAsyncResult BeginSend(SmtpConnection conn, ReadOnlyMemory command, MailAddress from, bool allowUnicode, AsyncCallback callback, object? state) + { + return TaskToAsyncResult.Begin(SendAsync(conn, command, from, allowUnicode), callback, state); + } + + internal static void EndSend(IAsyncResult asyncResult) + { + TaskToAsyncResult.End(asyncResult); + } + + internal static async Task SendAsync(SmtpConnection conn, ReadOnlyMemory command, MailAddress from, bool allowUnicode, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn, command, from, allowUnicode); - return CheckCommand.BeginSend(conn, callback, state); + LineInfo info = await CheckCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + CheckResponse(info.StatusCode, info.Line); } private static void CheckResponse(SmtpStatusCode statusCode, string response) @@ -543,14 +497,7 @@ private static void CheckResponse(SmtpStatusCode statusCode, string response) } } - internal static void EndSend(IAsyncResult result) - { - string response; - SmtpStatusCode statusCode = (SmtpStatusCode)CheckCommand.EndSend(result, out response); - CheckResponse(statusCode, response); - } - - private static void PrepareCommand(SmtpConnection conn, ReadOnlySpan command, MailAddress from, bool allowUnicode) + private static void PrepareCommand(SmtpConnection conn, ReadOnlyMemory command, MailAddress from, bool allowUnicode) { if (conn.IsStreamOpen) { @@ -565,22 +512,37 @@ private static void PrepareCommand(SmtpConnection conn, ReadOnlySpan comma } conn.BufferBuilder.Append(SmtpCommands.CRLF); } - - internal static void Send(SmtpConnection conn, ReadOnlySpan command, MailAddress from, bool allowUnicode) - { - PrepareCommand(conn, command, from, allowUnicode); - string response; - SmtpStatusCode statusCode = CheckCommand.Send(conn, out response); - CheckResponse(statusCode, response); - } } internal static class RecipientCommand { - internal static IAsyncResult BeginSend(SmtpConnection conn, string to, AsyncCallback? callback, object? state) + internal static bool Send(SmtpConnection conn, string to, out string response) + { + Task<(bool success, string response)> task = SendAsync(conn, to); + Debug.Assert(task.IsCompleted, "RecipientCommand.SendAsync should be completed synchronously."); + (bool success, string r) = task.GetAwaiter().GetResult(); + response = r; + return success; + } + + internal static IAsyncResult BeginSend(SmtpConnection conn, string to, AsyncCallback callback, object? state) + { + return TaskToAsyncResult.Begin(SendAsync(conn, to), callback, state); + } + + internal static bool EndSend(IAsyncResult asyncResult, out string response) + { + (bool success, string r) = TaskToAsyncResult.End<(bool success, string response)>(asyncResult); + response = r; + return success; + } + + internal static async Task<(bool success, string response)> SendAsync(SmtpConnection conn, string to, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn, to); - return CheckCommand.BeginSend(conn, callback, state); + LineInfo info = await CheckCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + return (CheckResponse(info.StatusCode, info.Line), info.Line); } private static bool CheckResponse(SmtpStatusCode statusCode, string response) @@ -613,12 +575,6 @@ private static bool CheckResponse(SmtpStatusCode statusCode, string response) } } - internal static bool EndSend(IAsyncResult result, out string response) - { - SmtpStatusCode statusCode = (SmtpStatusCode)CheckCommand.EndSend(result, out response); - return CheckResponse(statusCode, response); - } - private static void PrepareCommand(SmtpConnection conn, string to) { if (conn.IsStreamOpen) @@ -630,18 +586,35 @@ private static void PrepareCommand(SmtpConnection conn, string to) conn.BufferBuilder.Append(to, true); // Unicode validation was done prior conn.BufferBuilder.Append(SmtpCommands.CRLF); } + } + internal static class QuitCommand + { + internal static void Send(SmtpConnection conn) + { + Task task = SendAsync(conn); + Debug.Assert(task.IsCompleted, "QuitCommand.SendAsync should be completed synchronously."); + task.GetAwaiter().GetResult(); + } - internal static bool Send(SmtpConnection conn, string to, out string response) + internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback callback, object? state) { - PrepareCommand(conn, to); - SmtpStatusCode statusCode = CheckCommand.Send(conn, out response); - return CheckResponse(statusCode, response); + return TaskToAsyncResult.Begin(SendAsync(conn), callback, state); + } + + internal static void EndSend(IAsyncResult asyncResult) + { + TaskToAsyncResult.End(asyncResult); + } + + internal static async Task SendAsync(SmtpConnection conn, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter + { + PrepareCommand(conn); + await conn.FlushAsync(cancellationToken).ConfigureAwait(false); + // We don't read any response to match the synchronous behavior } - } - internal static class QuitCommand - { private static void PrepareCommand(SmtpConnection conn) { if (conn.IsStreamOpen) @@ -651,40 +624,29 @@ private static void PrepareCommand(SmtpConnection conn) conn.BufferBuilder.Append(SmtpCommands.Quit); } - - internal static void Send(SmtpConnection conn) - { - PrepareCommand(conn); - - // We simply flush and don't read the response - // to avoid blocking call that will impact users - // that are using async api, since this code - // will run on Dispose() - conn.Flush(); - } } internal static class SmtpCommands { - internal static ReadOnlySpan Auth => "AUTH "u8; - internal static ReadOnlySpan CRLF => "\r\n"u8; - internal static ReadOnlySpan Data => "DATA\r\n"u8; - internal static ReadOnlySpan DataStop => "\r\n.\r\n"u8; - internal static ReadOnlySpan EHello => "EHLO "u8; - internal static ReadOnlySpan Expand => "EXPN "u8; - internal static ReadOnlySpan Hello => "HELO "u8; - internal static ReadOnlySpan Help => "HELP"u8; - internal static ReadOnlySpan Mail => "MAIL FROM:"u8; - internal static ReadOnlySpan Noop => "NOOP\r\n"u8; - internal static ReadOnlySpan Quit => "QUIT\r\n"u8; - internal static ReadOnlySpan Recipient => "RCPT TO:"u8; - internal static ReadOnlySpan Reset => "RSET\r\n"u8; - internal static ReadOnlySpan Send => "SEND FROM:"u8; - internal static ReadOnlySpan SendAndMail => "SAML FROM:"u8; - internal static ReadOnlySpan SendOrMail => "SOML FROM:"u8; - internal static ReadOnlySpan Turn => "TURN\r\n"u8; - internal static ReadOnlySpan Verify => "VRFY "u8; - internal static ReadOnlySpan StartTls => "STARTTLS"u8; + internal static ReadOnlyMemory Auth => "AUTH "u8.ToArray(); + internal static ReadOnlyMemory CRLF => "\r\n"u8.ToArray(); + internal static ReadOnlyMemory Data => "DATA\r\n"u8.ToArray(); + internal static ReadOnlyMemory DataStop => "\r\n.\r\n"u8.ToArray(); + internal static ReadOnlyMemory EHello => "EHLO "u8.ToArray(); + internal static ReadOnlyMemory Expand => "EXPN "u8.ToArray(); + internal static ReadOnlyMemory Hello => "HELO "u8.ToArray(); + internal static ReadOnlyMemory Help => "HELP"u8.ToArray(); + internal static ReadOnlyMemory Mail => "MAIL FROM:"u8.ToArray(); + internal static ReadOnlyMemory Noop => "NOOP\r\n"u8.ToArray(); + internal static ReadOnlyMemory Quit => "QUIT\r\n"u8.ToArray(); + internal static ReadOnlyMemory Recipient => "RCPT TO:"u8.ToArray(); + internal static ReadOnlyMemory Reset => "RSET\r\n"u8.ToArray(); + internal static ReadOnlyMemory Send => "SEND FROM:"u8.ToArray(); + internal static ReadOnlyMemory SendAndMail => "SAML FROM:"u8.ToArray(); + internal static ReadOnlyMemory SendOrMail => "SOML FROM:"u8.ToArray(); + internal static ReadOnlyMemory Turn => "TURN\r\n"u8.ToArray(); + internal static ReadOnlyMemory Verify => "VRFY "u8.ToArray(); + internal static ReadOnlyMemory StartTls => "STARTTLS"u8.ToArray(); } internal readonly struct LineInfo diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpConnection.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpConnection.cs index 3dfc0d9214df77..e33e3da411a501 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpConnection.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpConnection.cs @@ -14,6 +14,7 @@ using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mail { @@ -105,19 +106,26 @@ internal IAsyncResult BeginGetConnection(ContextAwareResult outerResult, AsyncCa internal IAsyncResult BeginFlush(AsyncCallback? callback, object? state) { - return _stream!.BeginWrite(_bufferBuilder.GetBuffer(), 0, _bufferBuilder.Length, callback, state); + return TaskToAsyncResult.Begin(FlushAsync(CancellationToken.None), callback, state); } - internal void EndFlush(IAsyncResult result) + internal static void EndFlush(IAsyncResult result) { - _stream!.EndWrite(result); + TaskToAsyncResult.End(result); + } + + internal async Task FlushAsync(CancellationToken cancellationToken = default) where TIOAdapter : IReadWriteAdapter + { + await TIOAdapter.WriteAsync(_stream!, _bufferBuilder.GetBuffer().AsMemory(0, _bufferBuilder.Length), cancellationToken).ConfigureAwait(false); _bufferBuilder.Reset(); } + internal void Flush() { - _stream!.Write(_bufferBuilder.GetBuffer(), 0, _bufferBuilder.Length); - _bufferBuilder.Reset(); + Task task = FlushAsync(CancellationToken.None); + Debug.Assert(task.IsCompleted, "FlushAsync should be completed synchronously."); + task.GetAwaiter().GetResult(); } private void ShutdownConnection(bool isAbort) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpReplyReader.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpReplyReader.cs index 7887ac3c6d7f8a..a2de4cd060e0f4 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpReplyReader.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpReplyReader.cs @@ -2,13 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mail { //streams are read only; return of 0 means end of server's reply - internal sealed class SmtpReplyReader + internal sealed class SmtpReplyReader : IDisposable { + public void Dispose() + { + Close(); + } + private readonly SmtpReplyReaderFactory _reader; internal SmtpReplyReader(SmtpReplyReaderFactory reader) @@ -16,39 +24,65 @@ internal SmtpReplyReader(SmtpReplyReaderFactory reader) _reader = reader; } - internal IAsyncResult BeginReadLines(AsyncCallback? callback, object? state) + public void Close() { - return _reader.BeginReadLines(this, callback, state); + _reader.Close(this); } - internal IAsyncResult BeginReadLine(AsyncCallback? callback, object? state) + internal LineInfo[] ReadLines() { - return _reader.BeginReadLine(this, callback, state); + Task task = ReadLinesAsync(); + + Debug.Assert(task.IsCompleted, "ReadLinesAsync should be completed synchronously."); + return task.GetAwaiter().GetResult(); } - public void Close() + internal IAsyncResult BeginReadLines(AsyncCallback callback, object? state) { - _reader.Close(this); + return TaskToAsyncResult.Begin(ReadLinesAsync(), callback, state); } - internal static LineInfo[] EndReadLines(IAsyncResult result) + internal static LineInfo[] EndReadLines(IAsyncResult asyncResult) { - return SmtpReplyReaderFactory.EndReadLines(result); + return TaskToAsyncResult.End(asyncResult); } - internal static LineInfo EndReadLine(IAsyncResult result) + internal LineInfo ReadLine() { - return SmtpReplyReaderFactory.EndReadLine(result); + Task task = ReadLineAsync(); + + Debug.Assert(task.IsCompleted, "ReadLineAsync should be completed synchronously."); + return task.GetAwaiter().GetResult(); } - internal LineInfo[] ReadLines() + internal IAsyncResult BeginReadLine(AsyncCallback callback, object? state) { - return _reader.ReadLines(this); + return TaskToAsyncResult.Begin(ReadLineAsync(), callback, state); } - internal LineInfo ReadLine() + internal static LineInfo EndReadLine(IAsyncResult asyncResult) + { + return TaskToAsyncResult.End(asyncResult); + } + + internal Task ReadLinesAsync() + { + return ReadLinesAsync(); + } + + internal Task ReadLineAsync() + { + return ReadLineAsync(); + } + + internal Task ReadLinesAsync(CancellationToken cancellationToken = default) where TIOAdapter : IReadWriteAdapter + { + return _reader.ReadLinesAsync(this, false, cancellationToken); + } + + internal Task ReadLineAsync(CancellationToken cancellationToken = default) where TIOAdapter : IReadWriteAdapter { - return _reader.ReadLine(this); + return _reader.ReadLineAsync(this, cancellationToken); } } } diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpReplyReaderFactory.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpReplyReaderFactory.cs index 03d32a7e44d608..66be00167e3f02 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpReplyReaderFactory.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpReplyReaderFactory.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mail { @@ -53,20 +56,6 @@ internal SmtpStatusCode StatusCode } } - internal IAsyncResult BeginReadLines(SmtpReplyReader caller, AsyncCallback? callback, object? state) - { - ReadLinesAsyncResult result = new ReadLinesAsyncResult(this, callback, state); - result.Read(caller); - return result; - } - - internal IAsyncResult BeginReadLine(SmtpReplyReader caller, AsyncCallback? callback, object? state) - { - ReadLinesAsyncResult result = new ReadLinesAsyncResult(this, callback, state, true); - result.Read(caller); - return result; - } - internal void Close(SmtpReplyReader caller) { if (_currentReader == caller) @@ -82,21 +71,6 @@ internal void Close(SmtpReplyReader caller) } } - internal static LineInfo[] EndReadLines(IAsyncResult result) - { - return ReadLinesAsyncResult.End(result); - } - - internal static LineInfo EndReadLine(IAsyncResult result) - { - LineInfo[] info = ReadLinesAsyncResult.End(result); - if (info != null && info.Length > 0) - { - return info[0]; - } - return default; - } - internal SmtpReplyReader GetNextReplyReader() { _currentReader?.Close(); @@ -298,29 +272,48 @@ internal LineInfo[] ReadLines(SmtpReplyReader caller) } internal LineInfo[] ReadLines(SmtpReplyReader caller, bool oneLine) + { + Task task = ReadLinesAsync(caller, oneLine); + Debug.Assert(task.IsCompleted, "ReadLinesAsync should complete synchronously for SyncReadWriteAdapter"); + + return task.GetAwaiter().GetResult(); + } + + internal Task ReadLinesAsync(SmtpReplyReader caller, bool oneLine = false, CancellationToken cancellationToken = default) + { + return ReadLinesAsync(caller, oneLine, cancellationToken); + } + + internal async Task ReadLinesAsync(SmtpReplyReader caller, bool oneLine = false, CancellationToken cancellationToken = default) where TIOAdapter : IReadWriteAdapter { if (caller != _currentReader || _readState == ReadState.Done) { return Array.Empty(); } - _byteBuffer ??= new byte[SmtpReplyReaderFactory.DefaultBufferSize]; - + _byteBuffer ??= new byte[DefaultBufferSize]; System.Diagnostics.Debug.Assert(_readState == ReadState.Status0); var builder = new StringBuilder(); var lines = new List(); int statusRead = 0; - for (int start = 0, read = 0; ;) + int start = 0; + int read = 0; + + while (true) { if (start == read) { - read = _bufferedStream.Read(_byteBuffer); start = 0; + read = await TIOAdapter.ReadAsync(_bufferedStream, _byteBuffer, cancellationToken).ConfigureAwait(false); + if (read == 0) + { + throw new IOException(SR.Format(SR.net_io_readfailure, SR.net_io_connectionclosed)); + } } - int actual = ProcessRead(_byteBuffer.AsSpan(start, read), true); + int actual = ProcessRead(_byteBuffer!.AsSpan(start, read - start), true); if (statusRead < 4) { @@ -340,160 +333,36 @@ internal LineInfo[] ReadLines(SmtpReplyReader caller, bool oneLine) if (_readState == ReadState.Status0) { statusRead = 0; - lines.Add(new LineInfo(_statusCode, builder.ToString(0, builder.Length - 2))); // return everything except CRLF + lines.Add(new LineInfo(_statusCode, builder.ToString(0, builder.Length - 2))); // Exclude CRLF if (oneLine) { - _bufferedStream.Push(_byteBuffer.AsSpan(start, read - start)); + _bufferedStream.Push(_byteBuffer!.AsSpan(start, read - start)); return lines.ToArray(); } - builder = new StringBuilder(); + + builder.Clear(); } else if (_readState == ReadState.Done) { - lines.Add(new LineInfo(_statusCode, builder.ToString(0, builder.Length - 2))); // return everything except CRLF - _bufferedStream.Push(_byteBuffer.AsSpan(start, read - start)); + lines!.Add(new LineInfo(_statusCode, builder.ToString(0, builder.Length - 2))); // return everything except CRLF + _bufferedStream.Push(_byteBuffer!.AsSpan(start, read - start)); return lines.ToArray(); } } + } - private sealed class ReadLinesAsyncResult : LazyAsyncResult + internal async Task ReadLineAsync(SmtpReplyReader caller) { - private StringBuilder? _builder; - private List? _lines; - private readonly SmtpReplyReaderFactory _parent; - private static readonly AsyncCallback s_readCallback = new AsyncCallback(ReadCallback); - private int _read; - private int _statusRead; - private readonly bool _oneLine; - - internal ReadLinesAsyncResult(SmtpReplyReaderFactory parent, AsyncCallback? callback, object? state) : base(null, state, callback) - { - _parent = parent; - } - - internal ReadLinesAsyncResult(SmtpReplyReaderFactory parent, AsyncCallback? callback, object? state, bool oneLine) : base(null, state, callback) - { - _oneLine = oneLine; - _parent = parent; - } - - internal void Read(SmtpReplyReader caller) - { - // if we've already found the delimitter, then return 0 indicating - // end of stream. - if (_parent._currentReader != caller || _parent._readState == ReadState.Done) - { - InvokeCallback(); - return; - } - - _parent._byteBuffer ??= new byte[SmtpReplyReaderFactory.DefaultBufferSize]; - - System.Diagnostics.Debug.Assert(_parent._readState == ReadState.Status0); - - _builder = new StringBuilder(); - _lines = new List(); - - Read(); - } - - internal static LineInfo[] End(IAsyncResult result) - { - ReadLinesAsyncResult thisPtr = (ReadLinesAsyncResult)result; - thisPtr.InternalWaitForCompletion(); - return thisPtr._lines!.ToArray(); - } - - private void Read() - { - do - { - IAsyncResult result = _parent._bufferedStream.BeginRead(_parent._byteBuffer!, 0, _parent._byteBuffer!.Length, s_readCallback, this); - if (!result.CompletedSynchronously) - { - return; - } - _read = _parent._bufferedStream.EndRead(result); - } while (ProcessRead()); - } - - private static void ReadCallback(IAsyncResult result) - { - if (!result.CompletedSynchronously) - { - Exception? exception = null; - ReadLinesAsyncResult thisPtr = (ReadLinesAsyncResult)result.AsyncState!; - try - { - thisPtr._read = thisPtr._parent._bufferedStream.EndRead(result); - if (thisPtr.ProcessRead()) - { - thisPtr.Read(); - } - } - catch (Exception e) - { - exception = e; - } - - if (exception != null) - { - thisPtr.InvokeCallback(exception); - } - } - } - - private bool ProcessRead() - { - if (_read == 0) - { - throw new IOException(SR.Format(SR.net_io_readfailure, SR.net_io_connectionclosed)); - } - - for (int start = 0; start != _read;) - { - int actual = _parent.ProcessRead(_parent._byteBuffer!.AsSpan(start, _read - start), true); - - if (_statusRead < 4) - { - int left = Math.Min(4 - _statusRead, actual); - _statusRead += left; - start += left; - actual -= left; - if (actual == 0) - { - continue; - } - } - - _builder!.Append(Encoding.UTF8.GetString(_parent._byteBuffer!, start, actual)); - start += actual; + LineInfo[] lines = await ReadLinesAsync(caller, oneLine: true).ConfigureAwait(false); + return lines.Length > 0 ? lines[0] : default; + } - if (_parent._readState == ReadState.Status0) - { - _lines!.Add(new LineInfo(_parent._statusCode, _builder.ToString(0, _builder.Length - 2))); // return everything except CRLF - _builder = new StringBuilder(); - _statusRead = 0; - - if (_oneLine) - { - _parent._bufferedStream.Push(_parent._byteBuffer!.AsSpan(start, _read - start)); - InvokeCallback(); - return false; - } - } - else if (_parent._readState == ReadState.Done) - { - _lines!.Add(new LineInfo(_parent._statusCode, _builder.ToString(0, _builder.Length - 2))); // return everything except CRLF - _parent._bufferedStream.Push(_parent._byteBuffer!.AsSpan(start, _read - start)); - InvokeCallback(); - return false; - } - } - return true; - } + internal async Task ReadLineAsync(SmtpReplyReader caller, CancellationToken cancellationToken) where TIOAdapter : IReadWriteAdapter + { + LineInfo[] lines = await ReadLinesAsync(caller, oneLine: true, cancellationToken).ConfigureAwait(false); + return lines.Length > 0 ? lines[0] : default; } } } diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs index e06de7affa2e93..8370bf8a26c9ef 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs @@ -316,7 +316,7 @@ private void SendToCollection() { while (_toIndex < _toCollection.Count) { - MultiAsyncResult result = (MultiAsyncResult)RecipientCommand.BeginSend(_connection, + IAsyncResult result = RecipientCommand.BeginSend(_connection, _toCollection[_toIndex++].GetSmtpAddress(_allowUnicode) + _deliveryNotify, s_sendToCollectionCompleted, this); if (!result.CompletedSynchronously) diff --git a/src/libraries/System.Net.Mail/tests/Unit/System.Net.Mail.Unit.Tests.csproj b/src/libraries/System.Net.Mail/tests/Unit/System.Net.Mail.Unit.Tests.csproj index 03708443b2ea80..c59d1ab087d1d6 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/System.Net.Mail.Unit.Tests.csproj +++ b/src/libraries/System.Net.Mail/tests/Unit/System.Net.Mail.Unit.Tests.csproj @@ -110,6 +110,8 @@ Link="ProductionCode\MailHeaderInfo.cs" /> + Date: Wed, 7 May 2025 15:22:24 +0200 Subject: [PATCH 3/5] MailMessage and friends --- .../src/System/Net/Mail/MailMessage.cs | 27 ++- .../src/System/Net/Mail/MailPriority.cs | 100 +--------- .../src/System/Net/Mail/MailWriter.cs | 2 +- .../src/System/Net/Mail/SmtpClient.cs | 5 +- .../src/System/Net/Mime/BaseWriter.cs | 69 +------ .../src/System/Net/Mime/MimeBasePart.cs | 46 +---- .../src/System/Net/Mime/MimeMultiPart.cs | 182 +----------------- .../src/System/Net/Mime/MimePart.cs | 175 +---------------- .../src/System/Net/Mime/MimeWriter.cs | 28 +-- .../tests/Functional/MailMessageTest.cs | 13 +- 10 files changed, 55 insertions(+), 592 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailMessage.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailMessage.cs index c414b047f11235..6caf97c5d02504 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailMessage.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailMessage.cs @@ -3,10 +3,13 @@ using System; using System.Collections.Specialized; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Mime; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mail { @@ -429,22 +432,28 @@ private void SetContent(bool allowUnicode) } } - internal void Send(BaseWriter writer, bool sendEnvelope, bool allowUnicode) + internal void Send(BaseWriter writer, bool sendEnvelope, bool allowUnicode, CancellationToken cancellationToken = default) { - SetContent(allowUnicode); - _message.Send(writer, sendEnvelope, allowUnicode); + Task task = SendAsync(writer, sendEnvelope, allowUnicode, cancellationToken); + Debug.Assert(task.IsCompleted, "SendAsync should be completed synchronously."); + task.GetAwaiter().GetResult(); } - internal IAsyncResult BeginSend(BaseWriter writer, bool allowUnicode, - AsyncCallback? callback, object? state) + internal IAsyncResult BeginSend(BaseWriter writer, bool sendEnvelope, bool allowUnicode, AsyncCallback callback, object? state) { - SetContent(allowUnicode); - return _message.BeginSend(writer, allowUnicode, callback, state); + return TaskToAsyncResult.Begin(SendAsync(writer, sendEnvelope, allowUnicode), callback, state); + } + + internal static void EndSend(IAsyncResult asyncResult) + { + TaskToAsyncResult.End(asyncResult); } - internal void EndSend(IAsyncResult asyncResult) + internal async Task SendAsync(BaseWriter writer, bool sendEnvelope, bool allowUnicode, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { - _message.EndSend(asyncResult); + SetContent(allowUnicode); + await _message.SendAsync(writer, sendEnvelope, allowUnicode, cancellationToken).ConfigureAwait(false); } internal string BuildDeliveryStatusNotificationString() diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs index db52511ec6ebb6..09ed14abc2a294 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs @@ -5,6 +5,8 @@ using System.Net.Mime; using System.Runtime.ExceptionServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mail { @@ -139,7 +141,8 @@ internal string? Subject // extract the encoding from =?encoding?BorQ?blablalba?= inputEncoding = MimeBasePart.DecodeEncoding(value); } - catch (ArgumentException) { }; + catch (ArgumentException) { } + ; if (inputEncoding != null && value != null) { @@ -240,94 +243,8 @@ internal MimeBasePart? Content #region Sending - internal void EmptySendCallback(IAsyncResult result) - { - Exception? e = null; - - if (result.CompletedSynchronously) - { - return; - } - - EmptySendContext context = (EmptySendContext)result.AsyncState!; - try - { - BaseWriter.EndGetContentStream(result).Close(); - } - catch (Exception ex) - { - e = ex; - } - context._result.InvokeCallback(e); - } - - internal sealed class EmptySendContext - { - internal EmptySendContext(BaseWriter writer, LazyAsyncResult result) - { - _writer = writer; - _result = result; - } - - internal LazyAsyncResult _result; - internal BaseWriter _writer; - } - - internal IAsyncResult BeginSend(BaseWriter writer, bool allowUnicode, - AsyncCallback? callback, object? state) - { - PrepareHeaders(allowUnicode); - writer.WriteHeaders(Headers, allowUnicode); - - if (Content != null) - { - return Content.BeginSend(writer, callback, allowUnicode, state); - } - else - { - LazyAsyncResult result = new LazyAsyncResult(this, state, callback); - IAsyncResult newResult = writer.BeginGetContentStream(EmptySendCallback, new EmptySendContext(writer, result)); - if (newResult.CompletedSynchronously) - { - BaseWriter.EndGetContentStream(newResult).Close(); - result.InvokeCallback(); - } - return result; - } - } - - internal void EndSend(IAsyncResult asyncResult) - { - ArgumentNullException.ThrowIfNull(asyncResult); - - if (Content != null) - { - Content.EndSend(asyncResult); - } - else - { - LazyAsyncResult? castedAsyncResult = asyncResult as LazyAsyncResult; - - if (castedAsyncResult == null || castedAsyncResult.AsyncObject != this) - { - throw new ArgumentException(SR.net_io_invalidasyncresult); - } - - if (castedAsyncResult.EndCalled) - { - throw new InvalidOperationException(SR.Format(SR.net_io_invalidendcall, nameof(EndSend))); - } - - castedAsyncResult.InternalWaitForCompletion(); - castedAsyncResult.EndCalled = true; - if (castedAsyncResult.Result is Exception e) - { - ExceptionDispatchInfo.Throw(e); - } - } - } - - internal void Send(BaseWriter writer, bool sendEnvelope, bool allowUnicode) + internal async Task SendAsync(BaseWriter writer, bool sendEnvelope, bool allowUnicode, CancellationToken cancellationToken = default) + where TIOAdapter : IReadWriteAdapter { if (sendEnvelope) { @@ -340,11 +257,12 @@ internal void Send(BaseWriter writer, bool sendEnvelope, bool allowUnicode) if (Content != null) { - Content.Send(writer, allowUnicode); + await Content.SendAsync(writer, allowUnicode, cancellationToken).ConfigureAwait(false); } else { - writer.GetContentStream().Close(); + using var stream = writer.GetContentStream(); + // No content to write, just close the stream } } diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailWriter.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailWriter.cs index c643885d53a5ee..9b8ea2ead14788 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailWriter.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailWriter.cs @@ -40,7 +40,7 @@ internal override void WriteHeaders(NameValueCollection headers, bool allowUnico internal override void Close() { _bufferBuilder.Append("\r\n"u8); - Flush(null); + Flush(); _stream.Close(); } 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 884d3b18a38669..5daccb14d32079 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 @@ -892,7 +892,7 @@ private void SendMessageCallback(IAsyncResult result) { try { - _message!.EndSend(result); + MailMessage.EndSend(result); // If some recipients failed but not others, throw AFTER sending the message. Complete(_failedRecipientException, result.AsyncState!); } @@ -929,8 +929,7 @@ private void SendMailCallback(IAsyncResult result) } else { - _message!.BeginSend(_writer, - IsUnicodeSupported(), new AsyncCallback(SendMessageCallback), result.AsyncState!); + _message!.BeginSend(_writer, DeliveryMethod != SmtpDeliveryMethod.Network, IsUnicodeSupported(), new AsyncCallback(SendMessageCallback), result.AsyncState!); } } catch (Exception e) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mime/BaseWriter.cs b/src/libraries/System.Net.Mail/src/System/Net/Mime/BaseWriter.cs index 3b59251a0cccb6..8c0286cf1609eb 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mime/BaseWriter.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mime/BaseWriter.cs @@ -15,7 +15,6 @@ internal abstract class BaseWriter // In MailWriter, all encoding has already been done so this will only fold lines // that are NOT encoded already, which means being less conservative is ok. private const int DefaultLineLength = 76; - private static readonly AsyncCallback s_onWrite = OnWrite; protected readonly BufferBuilder _bufferBuilder; protected readonly Stream _stream; @@ -97,9 +96,7 @@ private void WriteAndFold(string value, int charsAlreadyOnLine, bool allowUnicod #region Content - internal Stream GetContentStream() => GetContentStream(null); - - private ClosableStream GetContentStream(MultiAsyncResult? multiResult) + internal Stream GetContentStream() { if (_isInContent) { @@ -111,84 +108,26 @@ private ClosableStream GetContentStream(MultiAsyncResult? multiResult) CheckBoundary(); _bufferBuilder.Append("\r\n"u8); - Flush(multiResult); + Flush(); ClosableStream cs = new ClosableStream(new EightBitStream(_stream, _shouldEncodeLeadingDots), _onCloseHandler); _contentStream = cs; return cs; } - internal IAsyncResult BeginGetContentStream(AsyncCallback? callback, object? state) - { - MultiAsyncResult multiResult = new MultiAsyncResult(this, callback, state); - - Stream s = GetContentStream(multiResult); - - if (!(multiResult.Result is Exception)) - { - multiResult.Result = s; - } - - multiResult.CompleteSequence(); - - return multiResult; - } - - internal static Stream EndGetContentStream(IAsyncResult result) - { - object o = MultiAsyncResult.End(result)!; - if (o is Exception e) - { - ExceptionDispatchInfo.Throw(e); - } - return (Stream)o; - } - #endregion Content #region Cleanup - protected void Flush(MultiAsyncResult? multiResult) + protected void Flush() { if (_bufferBuilder.Length > 0) { - if (multiResult != null) - { - multiResult.Enter(); - IAsyncResult result = _stream.BeginWrite(_bufferBuilder.GetBuffer(), 0, - _bufferBuilder.Length, s_onWrite, multiResult); - if (result.CompletedSynchronously) - { - _stream.EndWrite(result); - multiResult.Leave(); - } - } - else - { - _stream.Write(_bufferBuilder.GetBuffer(), 0, _bufferBuilder.Length); - } + _stream.Write(_bufferBuilder.GetBuffer(), 0, _bufferBuilder.Length); _bufferBuilder.Reset(); } } - protected static void OnWrite(IAsyncResult result) - { - if (!result.CompletedSynchronously) - { - MultiAsyncResult multiResult = (MultiAsyncResult)result.AsyncState!; - BaseWriter thisPtr = (BaseWriter)multiResult.Context; - try - { - thisPtr._stream.EndWrite(result); - multiResult.Leave(); - } - catch (Exception e) - { - multiResult.Leave(e); - } - } - } - internal abstract void Close(); protected abstract void OnClose(object? sender, EventArgs args); diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeBasePart.cs b/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeBasePart.cs index 804c067eb201cb..a8d2e0436faef6 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeBasePart.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeBasePart.cs @@ -4,10 +4,12 @@ using System.Collections.Specialized; using System.Net.Mail; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mime { - internal class MimeBasePart + internal abstract class MimeBasePart { internal const string DefaultCharSet = "utf-8"; @@ -186,46 +188,6 @@ internal void PrepareHeaders(bool allowUnicode) } } - internal virtual void Send(BaseWriter writer, bool allowUnicode) - { - throw new NotImplementedException(); - } - - internal virtual IAsyncResult BeginSend(BaseWriter writer, AsyncCallback? callback, - bool allowUnicode, object? state) - { - throw new NotImplementedException(); - } - - internal void EndSend(IAsyncResult asyncResult) - { - ArgumentNullException.ThrowIfNull(asyncResult); - - LazyAsyncResult? castedAsyncResult = asyncResult as MimePartAsyncResult; - - if (castedAsyncResult == null || castedAsyncResult.AsyncObject != this) - { - throw new ArgumentException(SR.net_io_invalidasyncresult, nameof(asyncResult)); - } - - if (castedAsyncResult.EndCalled) - { - throw new InvalidOperationException(SR.Format(SR.net_io_invalidendcall, nameof(EndSend))); - } - - castedAsyncResult.InternalWaitForCompletion(); - castedAsyncResult.EndCalled = true; - if (castedAsyncResult.Result is Exception) - { - throw (Exception)castedAsyncResult.Result; - } - } - - internal sealed class MimePartAsyncResult : LazyAsyncResult - { - internal MimePartAsyncResult(MimeBasePart part, object? state, AsyncCallback? callback) : base(part, state, callback) - { - } - } + internal abstract Task SendAsync(BaseWriter writer, bool allowUnicode, CancellationToken cancellationToken) where TIOAdapter : IReadWriteAdapter; } } diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeMultiPart.cs b/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeMultiPart.cs index b9b2038c507781..e0ffef86590615 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeMultiPart.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeMultiPart.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.ExceptionServices; using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mime { @@ -14,8 +15,6 @@ internal sealed class MimeMultiPart : MimeBasePart { private Collection? _parts; private static int s_boundary; - private AsyncCallback? _mimePartSentCallback; - private bool _allowUnicode; internal MimeMultiPart(MimeMultiPartType type) { @@ -42,182 +41,7 @@ private void SetType(MimeMultiPartType type) internal Collection Parts => _parts ??= new Collection(); - internal static void Complete(IAsyncResult result, Exception? e) - { - //if we already completed and we got called again, - //it mean's that there was an exception in the callback and we - //should just rethrow it. - - MimePartContext context = (MimePartContext)result.AsyncState!; - - if (context._completed) - { - ExceptionDispatchInfo.Throw(e!); - } - - try - { - context._outputStream!.Close(); - } - catch (Exception ex) - { - e ??= ex; - } - context._completed = true; - context._result.InvokeCallback(e); - } - - internal void MimeWriterCloseCallback(IAsyncResult result) - { - if (result.CompletedSynchronously) - { - return; - } - - ((MimePartContext)result.AsyncState!)._completedSynchronously = false; - - try - { - MimeWriterCloseCallbackHandler(result); - } - catch (Exception e) - { - Complete(result, e); - } - } - - private static void MimeWriterCloseCallbackHandler(IAsyncResult result) - { - MimePartContext context = (MimePartContext)result.AsyncState!; - ((MimeWriter)context._writer).EndClose(result); - Complete(result, null); - } - - internal void MimePartSentCallback(IAsyncResult result) - { - if (result.CompletedSynchronously) - { - return; - } - - ((MimePartContext)result.AsyncState!)._completedSynchronously = false; - - try - { - MimePartSentCallbackHandler(result); - } - catch (Exception e) - { - Complete(result, e); - } - } - - private void MimePartSentCallbackHandler(IAsyncResult result) - { - MimePartContext context = (MimePartContext)result.AsyncState!; - MimeBasePart part = (MimeBasePart)context._partsEnumerator.Current; - part.EndSend(result); - - if (context._partsEnumerator.MoveNext()) - { - part = (MimeBasePart)context._partsEnumerator.Current; - IAsyncResult sendResult = part.BeginSend(context._writer, _mimePartSentCallback!, _allowUnicode, context); - if (sendResult.CompletedSynchronously) - { - MimePartSentCallbackHandler(sendResult); - } - return; - } - else - { - IAsyncResult closeResult = ((MimeWriter)context._writer).BeginClose(new AsyncCallback(MimeWriterCloseCallback), context); - if (closeResult.CompletedSynchronously) - { - MimeWriterCloseCallbackHandler(closeResult); - } - } - } - - internal void ContentStreamCallback(IAsyncResult result) - { - if (result.CompletedSynchronously) - { - return; - } - - ((MimePartContext)result.AsyncState!)._completedSynchronously = false; - - try - { - ContentStreamCallbackHandler(result); - } - catch (Exception e) - { - Complete(result, e); - } - } - - private void ContentStreamCallbackHandler(IAsyncResult result) - { - MimePartContext context = (MimePartContext)result.AsyncState!; - context._outputStream = BaseWriter.EndGetContentStream(result); - context._writer = new MimeWriter(context._outputStream!, ContentType.Boundary!); - if (context._partsEnumerator.MoveNext()) - { - MimeBasePart part = (MimeBasePart)context._partsEnumerator.Current; - - _mimePartSentCallback = new AsyncCallback(MimePartSentCallback); - IAsyncResult sendResult = part.BeginSend(context._writer, _mimePartSentCallback, _allowUnicode, context); - if (sendResult.CompletedSynchronously) - { - MimePartSentCallbackHandler(sendResult); - } - return; - } - else - { - IAsyncResult closeResult = ((MimeWriter)context._writer).BeginClose(new AsyncCallback(MimeWriterCloseCallback), context); - if (closeResult.CompletedSynchronously) - { - MimeWriterCloseCallbackHandler(closeResult); - } - } - } - - internal override IAsyncResult BeginSend(BaseWriter writer, AsyncCallback? callback, bool allowUnicode, - object? state) - { - _allowUnicode = allowUnicode; - PrepareHeaders(allowUnicode); - writer.WriteHeaders(Headers, allowUnicode); - MimePartAsyncResult result = new MimePartAsyncResult(this, state, callback); - MimePartContext context = new MimePartContext(writer, result, Parts.GetEnumerator()); - IAsyncResult contentResult = writer.BeginGetContentStream(new AsyncCallback(ContentStreamCallback), context); - if (contentResult.CompletedSynchronously) - { - ContentStreamCallbackHandler(contentResult); - } - return result; - } - - internal sealed class MimePartContext - { - internal MimePartContext(BaseWriter writer, LazyAsyncResult result, IEnumerator partsEnumerator) - { - _writer = writer; - _result = result; - _partsEnumerator = partsEnumerator; - } - - internal IEnumerator _partsEnumerator; - internal Stream? _outputStream; - internal LazyAsyncResult _result; - internal BaseWriter _writer; - internal bool _completed; - internal bool _completedSynchronously = true; - } - - internal override void Send(BaseWriter writer, bool allowUnicode) + internal override async Task SendAsync(BaseWriter writer, bool allowUnicode, CancellationToken cancellationToken = default) { PrepareHeaders(allowUnicode); writer.WriteHeaders(Headers, allowUnicode); @@ -226,7 +50,7 @@ internal override void Send(BaseWriter writer, bool allowUnicode) foreach (MimeBasePart part in Parts) { - part.Send(mimeWriter, allowUnicode); + await part.SendAsync(mimeWriter, allowUnicode, cancellationToken).ConfigureAwait(false); } mimeWriter.Close(); diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mime/MimePart.cs b/src/libraries/System.Net.Mail/src/System/Net/Mime/MimePart.cs index 85d20815d71f07..c95cf4f0346e27 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mime/MimePart.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mime/MimePart.cs @@ -8,6 +8,8 @@ using System.Net.Mail; using System.Runtime.ExceptionServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Net.Mime { @@ -19,8 +21,6 @@ internal sealed class MimePart : MimeBasePart, IDisposable private Stream? _stream; private bool _streamSet; private bool _streamUsedOnce; - private AsyncCallback? _readCallback; - private AsyncCallback? _writeCallback; private const int maxBufferSize = 0x4400; //seems optimal for send based on perf analysis internal MimePart() { } @@ -139,98 +139,6 @@ internal void SetContent(Stream stream, ContentType? contentType) SetContent(stream); } - internal static void Complete(IAsyncResult result, Exception? e) - { - //if we already completed and we got called again, - //it mean's that there was an exception in the callback and we - //should just rethrow it. - - MimePartContext context = (MimePartContext)result.AsyncState!; - if (context._completed) - { - ExceptionDispatchInfo.Throw(e!); - } - - try - { - context._outputStream?.Close(); - } - catch (Exception ex) - { - e ??= ex; - } - context._completed = true; - context._result.InvokeCallback(e); - } - - - internal void ReadCallback(IAsyncResult result) - { - if (result.CompletedSynchronously) - { - return; - } - - ((MimePartContext)result.AsyncState!)._completedSynchronously = false; - - try - { - ReadCallbackHandler(result); - } - catch (Exception e) - { - Complete(result, e); - } - } - - internal void ReadCallbackHandler(IAsyncResult result) - { - MimePartContext context = (MimePartContext)result.AsyncState!; - context._bytesLeft = Stream!.EndRead(result); - if (context._bytesLeft > 0) - { - IAsyncResult writeResult = context._outputStream!.BeginWrite(context._buffer, 0, context._bytesLeft, _writeCallback, context); - if (writeResult.CompletedSynchronously) - { - WriteCallbackHandler(writeResult); - } - } - else - { - Complete(result, null); - } - } - - internal void WriteCallback(IAsyncResult result) - { - if (result.CompletedSynchronously) - { - return; - } - - ((MimePartContext)result.AsyncState!)._completedSynchronously = false; - - try - { - WriteCallbackHandler(result); - } - catch (Exception e) - { - Complete(result, e); - } - } - - internal void WriteCallbackHandler(IAsyncResult result) - { - MimePartContext context = (MimePartContext)result.AsyncState!; - context._outputStream!.EndWrite(result); - IAsyncResult readResult = Stream!.BeginRead(context._buffer, 0, context._buffer.Length, _readCallback, context); - if (readResult.CompletedSynchronously) - { - ReadCallbackHandler(readResult); - } - } - internal Stream GetEncodedStream(Stream stream) { Stream outputStream = stream; @@ -251,76 +159,7 @@ internal Stream GetEncodedStream(Stream stream) return outputStream; } - internal void ContentStreamCallbackHandler(IAsyncResult result) - { - MimePartContext context = (MimePartContext)result.AsyncState!; - Stream outputStream = BaseWriter.EndGetContentStream(result); - context._outputStream = GetEncodedStream(outputStream); - - _readCallback = new AsyncCallback(ReadCallback); - _writeCallback = new AsyncCallback(WriteCallback); - IAsyncResult readResult = Stream!.BeginRead(context._buffer, 0, context._buffer.Length, _readCallback, context); - if (readResult.CompletedSynchronously) - { - ReadCallbackHandler(readResult); - } - } - - internal void ContentStreamCallback(IAsyncResult result) - { - if (result.CompletedSynchronously) - { - return; - } - - ((MimePartContext)result.AsyncState!)._completedSynchronously = false; - - try - { - ContentStreamCallbackHandler(result); - } - catch (Exception e) - { - Complete(result, e); - } - } - - internal sealed class MimePartContext - { - internal MimePartContext(BaseWriter writer, LazyAsyncResult result) - { - _writer = writer; - _result = result; - _buffer = new byte[maxBufferSize]; - } - - internal Stream? _outputStream; - internal LazyAsyncResult _result; - internal int _bytesLeft; - internal BaseWriter _writer; - internal byte[] _buffer; - internal bool _completed; - internal bool _completedSynchronously = true; - } - - internal override IAsyncResult BeginSend(BaseWriter writer, AsyncCallback? callback, bool allowUnicode, object? state) - { - PrepareHeaders(allowUnicode); - writer.WriteHeaders(Headers, allowUnicode); - MimePartAsyncResult result = new MimePartAsyncResult(this, state, callback); - MimePartContext context = new MimePartContext(writer, result); - - ResetStream(); - _streamUsedOnce = true; - IAsyncResult contentResult = writer.BeginGetContentStream(new AsyncCallback(ContentStreamCallback), context); - if (contentResult.CompletedSynchronously) - { - ContentStreamCallbackHandler(contentResult); - } - return result; - } - - internal override void Send(BaseWriter writer, bool allowUnicode) + internal override async Task SendAsync(BaseWriter writer, bool allowUnicode, CancellationToken cancellationToken = default) { if (Stream != null) { @@ -332,15 +171,15 @@ internal override void Send(BaseWriter writer, bool allowUnicode) Stream outputStream = writer.GetContentStream(); outputStream = GetEncodedStream(outputStream); - int read; - ResetStream(); _streamUsedOnce = true; - while ((read = Stream.Read(buffer, 0, maxBufferSize)) > 0) + int read; + while ((read = await TIOAdapter.ReadAsync(Stream, buffer.AsMemory(0, maxBufferSize), cancellationToken).ConfigureAwait(false)) > 0) { - outputStream.Write(buffer, 0, read); + await TIOAdapter.WriteAsync(outputStream, buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); } + outputStream.Close(); } } diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeWriter.cs b/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeWriter.cs index 8d9482b8ee1aa9..bb8fd314104cd2 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeWriter.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mime/MimeWriter.cs @@ -35,37 +35,13 @@ internal override void WriteHeaders(NameValueCollection headers, bool allowUnico #region Cleanup - internal IAsyncResult BeginClose(AsyncCallback? callback, object? state) - { - MultiAsyncResult multiResult = new MultiAsyncResult(this, callback, state); - - Close(multiResult); - - multiResult.CompleteSequence(); - - return multiResult; - } - - internal void EndClose(IAsyncResult result) - { - MultiAsyncResult.End(result); - - _stream.Close(); - } - internal override void Close() - { - Close(null); - - _stream.Close(); - } - - private void Close(MultiAsyncResult? multiResult) { _bufferBuilder.Append("\r\n--"u8); _bufferBuilder.Append(_boundaryBytes); _bufferBuilder.Append("--\r\n"u8); - Flush(multiResult); + Flush(); + _stream.Close(); } /// diff --git a/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs b/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs index 757383276777e1..b42390d49ada58 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs @@ -261,17 +261,14 @@ private static (string Raw, string Attachment) DecodeSentMailMessage(MailMessage culture: null, activationAttributes: null); - // var syncSendAdapterType = typeof(MailMessage).Assembly.GetTypes() - // .FirstOrDefault(t => t.Name == "SyncReadWriteAdapter"); + var syncSendAdapterType = typeof(MailMessage).Assembly.GetTypes() + .FirstOrDefault(t => t.Name == "SyncReadWriteAdapter"); // Send the message. - // typeof(MailMessage) - // .GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic) - // .MakeGenericMethod(syncSendAdapterType) - // .Invoke(mail, new object[] { mailWriter, true, true, CancellationToken.None }); typeof(MailMessage) - .GetMethod("Send", BindingFlags.Instance | BindingFlags.NonPublic) - .Invoke(mail, new object[] { mailWriter, true, true }); + .GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic) + .MakeGenericMethod(syncSendAdapterType) + .Invoke(mail, new object[] { mailWriter, true, true, CancellationToken.None }); // Decode contents. string result = Encoding.UTF8.GetString(stream.ToArray()); From 1ca1e3655271e70801b3abc1d201dcc12bcb714c Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Fri, 16 May 2025 09:00:34 +0200 Subject: [PATCH 4/5] Remove allocations in SmtpCommands --- .../src/System/Net/Mail/SmtpCommands.cs | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs index e8b5cae5cfe960..db2c186963bc8b 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs @@ -127,7 +127,7 @@ private static LineInfo CheckResponse(LineInfo[] lines) private static void PrepareCommand(SmtpConnection conn, string type, string message) { - conn.BufferBuilder.Append(SmtpCommands.Auth.Span); + conn.BufferBuilder.Append(SmtpCommands.Auth); conn.BufferBuilder.Append(type); conn.BufferBuilder.Append((byte)' '); conn.BufferBuilder.Append(message); @@ -449,14 +449,14 @@ private static void PrepareCommand(SmtpConnection conn) internal static class MailCommand { - internal static void Send(SmtpConnection conn, ReadOnlyMemory command, MailAddress from, bool allowUnicode) + internal static void Send(SmtpConnection conn, ReadOnlySpan command, MailAddress from, bool allowUnicode) { Task task = SendAsync(conn, command, from, allowUnicode); Debug.Assert(task.IsCompleted, "MailCommand.SendAsync should be completed synchronously."); task.GetAwaiter().GetResult(); } - internal static IAsyncResult BeginSend(SmtpConnection conn, ReadOnlyMemory command, MailAddress from, bool allowUnicode, AsyncCallback callback, object? state) + internal static IAsyncResult BeginSend(SmtpConnection conn, ReadOnlySpan command, MailAddress from, bool allowUnicode, AsyncCallback callback, object? state) { return TaskToAsyncResult.Begin(SendAsync(conn, command, from, allowUnicode), callback, state); } @@ -466,12 +466,18 @@ internal static void EndSend(IAsyncResult asyncResult) TaskToAsyncResult.End(asyncResult); } - internal static async Task SendAsync(SmtpConnection conn, ReadOnlyMemory command, MailAddress from, bool allowUnicode, CancellationToken cancellationToken = default) + internal static Task SendAsync(SmtpConnection conn, ReadOnlySpan command, MailAddress from, bool allowUnicode, CancellationToken cancellationToken = default) where TIOAdapter : IReadWriteAdapter { PrepareCommand(conn, command, from, allowUnicode); - LineInfo info = await CheckCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); - CheckResponse(info.StatusCode, info.Line); + return SendAndCheck(conn, cancellationToken); + + static async Task SendAndCheck(SmtpConnection conn, CancellationToken cancellationToken) + { + LineInfo info = await CheckCommand.SendAsync(conn, cancellationToken).ConfigureAwait(false); + CheckResponse(info.StatusCode, info.Line); + return info; + } } private static void CheckResponse(SmtpStatusCode statusCode, string response) @@ -497,7 +503,7 @@ private static void CheckResponse(SmtpStatusCode statusCode, string response) } } - private static void PrepareCommand(SmtpConnection conn, ReadOnlyMemory command, MailAddress from, bool allowUnicode) + private static void PrepareCommand(SmtpConnection conn, ReadOnlySpan command, MailAddress from, bool allowUnicode) { if (conn.IsStreamOpen) { @@ -628,25 +634,25 @@ private static void PrepareCommand(SmtpConnection conn) internal static class SmtpCommands { - internal static ReadOnlyMemory Auth => "AUTH "u8.ToArray(); - internal static ReadOnlyMemory CRLF => "\r\n"u8.ToArray(); - internal static ReadOnlyMemory Data => "DATA\r\n"u8.ToArray(); - internal static ReadOnlyMemory DataStop => "\r\n.\r\n"u8.ToArray(); - internal static ReadOnlyMemory EHello => "EHLO "u8.ToArray(); - internal static ReadOnlyMemory Expand => "EXPN "u8.ToArray(); - internal static ReadOnlyMemory Hello => "HELO "u8.ToArray(); - internal static ReadOnlyMemory Help => "HELP"u8.ToArray(); - internal static ReadOnlyMemory Mail => "MAIL FROM:"u8.ToArray(); - internal static ReadOnlyMemory Noop => "NOOP\r\n"u8.ToArray(); - internal static ReadOnlyMemory Quit => "QUIT\r\n"u8.ToArray(); - internal static ReadOnlyMemory Recipient => "RCPT TO:"u8.ToArray(); - internal static ReadOnlyMemory Reset => "RSET\r\n"u8.ToArray(); - internal static ReadOnlyMemory Send => "SEND FROM:"u8.ToArray(); - internal static ReadOnlyMemory SendAndMail => "SAML FROM:"u8.ToArray(); - internal static ReadOnlyMemory SendOrMail => "SOML FROM:"u8.ToArray(); - internal static ReadOnlyMemory Turn => "TURN\r\n"u8.ToArray(); - internal static ReadOnlyMemory Verify => "VRFY "u8.ToArray(); - internal static ReadOnlyMemory StartTls => "STARTTLS"u8.ToArray(); + internal static ReadOnlySpan Auth => "AUTH "u8; + internal static ReadOnlySpan CRLF => "\r\n"u8; + internal static ReadOnlySpan Data => "DATA\r\n"u8; + internal static ReadOnlySpan DataStop => "\r\n.\r\n"u8; + internal static ReadOnlySpan EHello => "EHLO "u8; + internal static ReadOnlySpan Expand => "EXPN "u8; + internal static ReadOnlySpan Hello => "HELO "u8; + internal static ReadOnlySpan Help => "HELP"u8; + internal static ReadOnlySpan Mail => "MAIL FROM:"u8; + internal static ReadOnlySpan Noop => "NOOP\r\n"u8; + internal static ReadOnlySpan Quit => "QUIT\r\n"u8; + internal static ReadOnlySpan Recipient => "RCPT TO:"u8; + internal static ReadOnlySpan Reset => "RSET\r\n"u8; + internal static ReadOnlySpan Send => "SEND FROM:"u8; + internal static ReadOnlySpan SendAndMail => "SAML FROM:"u8; + internal static ReadOnlySpan SendOrMail => "SOML FROM:"u8; + internal static ReadOnlySpan Turn => "TURN\r\n"u8; + internal static ReadOnlySpan Verify => "VRFY "u8; + internal static ReadOnlySpan StartTls => "STARTTLS"u8; } internal readonly struct LineInfo From a79732a664f84ec173bc803092e5c6ee81f73a09 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Fri, 16 May 2025 09:05:04 +0200 Subject: [PATCH 5/5] More feedback --- .../src/System/Net/Mail/MailPriority.cs | 7 ++++--- .../src/System/Net/Mail/SmtpCommands.cs | 10 ---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs index 09ed14abc2a294..f45299e71152ed 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs @@ -141,8 +141,9 @@ internal string? Subject // extract the encoding from =?encoding?BorQ?blablalba?= inputEncoding = MimeBasePart.DecodeEncoding(value); } - catch (ArgumentException) { } - ; + catch (ArgumentException) + { + } if (inputEncoding != null && value != null) { @@ -261,8 +262,8 @@ internal async Task SendAsync(BaseWriter writer, bool sendEnvelope, } else { - using var stream = writer.GetContentStream(); // No content to write, just close the stream + writer.GetContentStream().Close(); } } diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs index db2c186963bc8b..5c116f579607ec 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs @@ -23,16 +23,6 @@ internal static LineInfo Send(SmtpConnection conn) return task.GetAwaiter().GetResult(); } - internal static IAsyncResult BeginSend(SmtpConnection conn, AsyncCallback callback, object? state) - { - return TaskToAsyncResult.Begin(SendAsync(conn), callback, state); - } - - internal static LineInfo EndSend(IAsyncResult asyncResult) - { - return TaskToAsyncResult.End(asyncResult); - } - internal static async Task SendAsync(SmtpConnection conn, CancellationToken cancellationToken = default) where TIOAdapter : IReadWriteAdapter {