File: System\Net\Mail\SmtpTransport.cs
Web Access
Project: src\src\libraries\System.Net.Mail\src\System.Net.Mail.csproj (System.Net.Mail)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.IO;
using System.Net.Mime;
using System.Runtime.ExceptionServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
 
namespace System.Net.Mail
{
    internal sealed class SmtpTransport
    {
        internal const int DefaultPort = 25;
 
        private readonly ISmtpAuthenticationModule[] _authenticationModules;
        private SmtpConnection? _connection;
        private readonly SmtpClient _client;
        private ICredentialsByHost? _credentials;
        private readonly List<SmtpFailedRecipientException> _failedRecipientExceptions = new List<SmtpFailedRecipientException>();
        private bool _identityRequired;
        private bool _shouldAbort;
 
        private bool _enableSsl;
        private X509CertificateCollection? _clientCertificates;
 
        internal SmtpTransport(SmtpClient client) : this(client, SmtpAuthenticationManager.GetModules())
        {
        }
 
        internal SmtpTransport(SmtpClient client, ISmtpAuthenticationModule[] authenticationModules)
        {
            ArgumentNullException.ThrowIfNull(authenticationModules);
 
            _client = client;
            _authenticationModules = authenticationModules;
        }
 
        internal ICredentialsByHost? Credentials
        {
            get
            {
                return _credentials;
            }
            set
            {
                _credentials = value;
            }
        }
 
        internal bool IdentityRequired
        {
            get
            {
                return _identityRequired;
            }
 
            set
            {
                _identityRequired = value;
            }
        }
 
        internal bool IsConnected
        {
            get
            {
                return _connection != null && _connection.IsConnected;
            }
        }
 
        internal bool EnableSsl
        {
            get
            {
                return _enableSsl;
            }
            set
            {
                _enableSsl = value;
            }
        }
 
        internal X509CertificateCollection ClientCertificates => _clientCertificates ??= new X509CertificateCollection();
 
        internal bool ServerSupportsEai
        {
            get { return _connection != null && _connection.ServerSupportsEai; }
        }
 
        internal void GetConnection(string host, int port)
        {
            try
            {
                lock (this)
                {
                    _connection = new SmtpConnection(this, _client, _credentials, _authenticationModules);
                    if (_shouldAbort)
                    {
                        _connection.Abort();
                    }
                    _shouldAbort = false;
                }
 
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Associate(this, _connection);
 
                if (EnableSsl)
                {
                    _connection.EnableSsl = true;
                    _connection.ClientCertificates = ClientCertificates;
                }
 
                _connection.GetConnection(host, port);
            }
            finally { }
        }
 
        internal IAsyncResult BeginGetConnection(ContextAwareResult outerResult, AsyncCallback? callback, object? state, string host, int port)
        {
            IAsyncResult? result = null;
            try
            {
                _connection = new SmtpConnection(this, _client, _credentials, _authenticationModules);
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Associate(this, _connection);
                if (EnableSsl)
                {
                    _connection.EnableSsl = true;
                    _connection.ClientCertificates = ClientCertificates;
                }
 
                result = _connection.BeginGetConnection(outerResult, callback, state, host, port);
            }
            catch (Exception innerException)
            {
                throw new SmtpException(SR.MailHostNotFound, innerException);
            }
 
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Sync completion");
 
            return result;
        }
 
        internal static void EndGetConnection(IAsyncResult result)
        {
            SmtpConnection.EndGetConnection(result);
        }
 
        internal IAsyncResult BeginSendMail(MailAddress sender, MailAddressCollection recipients,
            string deliveryNotify, bool allowUnicode, AsyncCallback? callback, object? state)
        {
            ArgumentNullException.ThrowIfNull(sender);
            ArgumentNullException.ThrowIfNull(recipients);
 
            SendMailAsyncResult result = new SendMailAsyncResult(_connection!, sender, recipients,
                allowUnicode, _connection!.DSNEnabled ? deliveryNotify : null,
                callback, state);
            result.Send();
            return result;
        }
 
        internal void ReleaseConnection()
        {
            _connection?.ReleaseConnection();
        }
 
        internal void Abort()
        {
            lock (this)
            {
                if (_connection != null)
                {
                    _connection.Abort();
                }
                else
                {
                    _shouldAbort = true;
                }
            }
        }
 
        internal static MailWriter EndSendMail(IAsyncResult result)
        {
            try
            {
                return SendMailAsyncResult.End(result);
            }
            finally
            {
            }
        }
 
        internal MailWriter SendMail(MailAddress sender, MailAddressCollection recipients, string deliveryNotify,
            bool allowUnicode, out SmtpFailedRecipientException? exception)
        {
            ArgumentNullException.ThrowIfNull(sender);
            ArgumentNullException.ThrowIfNull(recipients);
 
            MailCommand.Send(_connection!, SmtpCommands.Mail, sender, allowUnicode);
            _failedRecipientExceptions.Clear();
 
            exception = null;
            string response;
            foreach (MailAddress address in recipients)
            {
                string smtpAddress = address.GetSmtpAddress(allowUnicode);
                string to = smtpAddress + (_connection!.DSNEnabled ? deliveryNotify : string.Empty);
                if (!RecipientCommand.Send(_connection, to, out response))
                {
                    _failedRecipientExceptions.Add(
                        new SmtpFailedRecipientException(_connection.Reader!.StatusCode, smtpAddress, response));
                }
            }
 
            if (_failedRecipientExceptions.Count > 0)
            {
                if (_failedRecipientExceptions.Count == 1)
                {
                    exception = _failedRecipientExceptions[0];
                }
                else
                {
                    exception = new SmtpFailedRecipientsException(_failedRecipientExceptions, _failedRecipientExceptions.Count == recipients.Count);
                }
 
                if (_failedRecipientExceptions.Count == recipients.Count)
                {
                    exception.fatal = true;
                    throw exception;
                }
            }
 
            DataCommand.Send(_connection!);
            return new MailWriter(_connection!.GetClosableStream(), encodeForTransport: true);
        }
    }
 
    internal sealed class SendMailAsyncResult : LazyAsyncResult
    {
        private readonly SmtpConnection _connection;
        private readonly MailAddress _from;
        private readonly string? _deliveryNotify;
        private static readonly AsyncCallback s_sendMailFromCompleted = new AsyncCallback(SendMailFromCompleted);
        private static readonly AsyncCallback s_sendToCollectionCompleted = new AsyncCallback(SendToCollectionCompleted);
        private static readonly AsyncCallback s_sendDataCompleted = new AsyncCallback(SendDataCompleted);
        private readonly List<SmtpFailedRecipientException> _failedRecipientExceptions = new List<SmtpFailedRecipientException>();
        private Stream? _stream;
        private readonly MailAddressCollection _toCollection;
        private int _toIndex;
        private readonly bool _allowUnicode;
 
 
        internal SendMailAsyncResult(SmtpConnection connection, MailAddress from, MailAddressCollection toCollection,
            bool allowUnicode, string? deliveryNotify, AsyncCallback? callback, object? state)
            : base(null, state, callback)
        {
            _toCollection = toCollection;
            _connection = connection;
            _from = from;
            _deliveryNotify = deliveryNotify;
            _allowUnicode = allowUnicode;
        }
 
        internal void Send()
        {
            SendMailFrom();
        }
 
        internal static MailWriter End(IAsyncResult result)
        {
            SendMailAsyncResult thisPtr = (SendMailAsyncResult)result;
            object? sendMailResult = thisPtr.InternalWaitForCompletion();
 
            // Note the difference between the singular and plural FailedRecipient exceptions.
            // Only fail immediately if we couldn't send to any recipients.
            if ((sendMailResult is Exception e)
                && (!(sendMailResult is SmtpFailedRecipientException)
                    || ((SmtpFailedRecipientException)sendMailResult).fatal))
            {
                ExceptionDispatchInfo.Throw(e);
            }
 
            return new MailWriter(thisPtr._stream!, encodeForTransport: true);
        }
        private void SendMailFrom()
        {
            IAsyncResult result = MailCommand.BeginSend(_connection, SmtpCommands.Mail, _from, _allowUnicode,
                s_sendMailFromCompleted, this);
            if (!result.CompletedSynchronously)
            {
                return;
            }
 
            MailCommand.EndSend(result);
            SendToCollection();
        }
 
        private static void SendMailFromCompleted(IAsyncResult result)
        {
            if (!result.CompletedSynchronously)
            {
                SendMailAsyncResult thisPtr = (SendMailAsyncResult)result.AsyncState!;
                try
                {
                    MailCommand.EndSend(result);
                    thisPtr.SendToCollection();
                }
                catch (Exception e)
                {
                    thisPtr.InvokeCallback(e);
                }
            }
        }
 
        private void SendToCollection()
        {
            while (_toIndex < _toCollection.Count)
            {
                MultiAsyncResult result = (MultiAsyncResult)RecipientCommand.BeginSend(_connection,
                    _toCollection[_toIndex++].GetSmtpAddress(_allowUnicode) + _deliveryNotify,
                    s_sendToCollectionCompleted, this);
                if (!result.CompletedSynchronously)
                {
                    return;
                }
                string response;
                if (!RecipientCommand.EndSend(result, out response))
                {
                    _failedRecipientExceptions.Add(new SmtpFailedRecipientException(_connection.Reader!.StatusCode,
                        _toCollection[_toIndex - 1].GetSmtpAddress(_allowUnicode), response));
                }
            }
            SendData();
        }
 
        private static void SendToCollectionCompleted(IAsyncResult result)
        {
            if (!result.CompletedSynchronously)
            {
                SendMailAsyncResult thisPtr = (SendMailAsyncResult)result.AsyncState!;
                try
                {
                    string response;
                    if (!RecipientCommand.EndSend(result, out response))
                    {
                        thisPtr._failedRecipientExceptions.Add(
                            new SmtpFailedRecipientException(thisPtr._connection.Reader!.StatusCode,
                                thisPtr._toCollection[thisPtr._toIndex - 1].GetSmtpAddress(thisPtr._allowUnicode),
                                response));
 
                        if (thisPtr._failedRecipientExceptions.Count == thisPtr._toCollection.Count)
                        {
                            SmtpFailedRecipientException exception = thisPtr._toCollection.Count == 1 ?
                                (SmtpFailedRecipientException)thisPtr._failedRecipientExceptions[0] :
                                new SmtpFailedRecipientsException(thisPtr._failedRecipientExceptions, true);
                            exception.fatal = true;
                            thisPtr.InvokeCallback(exception);
                            return;
                        }
                    }
                    thisPtr.SendToCollection();
                }
                catch (Exception e)
                {
                    thisPtr.InvokeCallback(e);
                }
            }
        }
 
        private void SendData()
        {
            IAsyncResult result = DataCommand.BeginSend(_connection, s_sendDataCompleted, this);
            if (!result.CompletedSynchronously)
            {
                return;
            }
            DataCommand.EndSend(result);
            _stream = _connection.GetClosableStream();
            if (_failedRecipientExceptions.Count > 1)
            {
                InvokeCallback(new SmtpFailedRecipientsException(_failedRecipientExceptions, _failedRecipientExceptions.Count == _toCollection.Count));
            }
            else if (_failedRecipientExceptions.Count == 1)
            {
                InvokeCallback(_failedRecipientExceptions[0]);
            }
            else
            {
                InvokeCallback();
            }
        }
 
        private static void SendDataCompleted(IAsyncResult result)
        {
            if (!result.CompletedSynchronously)
            {
                SendMailAsyncResult thisPtr = (SendMailAsyncResult)result.AsyncState!;
                try
                {
                    DataCommand.EndSend(result);
                    thisPtr._stream = thisPtr._connection.GetClosableStream();
                    if (thisPtr._failedRecipientExceptions.Count > 1)
                    {
                        thisPtr.InvokeCallback(new SmtpFailedRecipientsException(thisPtr._failedRecipientExceptions, thisPtr._failedRecipientExceptions.Count == thisPtr._toCollection.Count));
                    }
                    else if (thisPtr._failedRecipientExceptions.Count == 1)
                    {
                        thisPtr.InvokeCallback(thisPtr._failedRecipientExceptions[0]);
                    }
                    else
                    {
                        thisPtr.InvokeCallback();
                    }
                }
                catch (Exception e)
                {
                    thisPtr.InvokeCallback(e);
                }
            }
        }
 
        // Return the list of non-terminal failures (some recipients failed but not others).
        internal SmtpFailedRecipientException? GetFailedRecipientException()
        {
            if (_failedRecipientExceptions.Count == 1)
            {
                return (SmtpFailedRecipientException)_failedRecipientExceptions[0];
            }
            else if (_failedRecipientExceptions.Count > 1)
            {
                // Aggregate exception, multiple failures
                return new SmtpFailedRecipientsException(_failedRecipientExceptions, false);
            }
            return null;
        }
    }
}