File: System\Net\Mail\SmtpConnection.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;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.ExceptionServices;
using System.Security.Authentication;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.Mail
{
    internal sealed partial class SmtpConnection
    {
        private readonly BufferBuilder _bufferBuilder = new BufferBuilder();
        private bool _isConnected;
        private bool _isClosed;
        private bool _isStreamOpen;
        private readonly EventHandler? _onCloseHandler;
        internal SmtpTransport? _parent;
        private readonly SmtpClient? _client;
        private Stream? _stream;
        internal TcpClient? _tcpClient;
        private SmtpReplyReaderFactory? _responseReader;
 
        private readonly ICredentialsByHost? _credentials;
        private string[]? _extensions;
        private bool _enableSsl;
        private X509CertificateCollection? _clientCertificates;
 
        internal SmtpConnection(SmtpTransport parent, SmtpClient client, ICredentialsByHost? credentials, ISmtpAuthenticationModule[] authenticationModules)
        {
            _client = client;
            _credentials = credentials;
            _authenticationModules = authenticationModules;
            _parent = parent;
            _tcpClient = new TcpClient();
            _onCloseHandler = new EventHandler(OnClose);
        }
 
        internal BufferBuilder BufferBuilder => _bufferBuilder;
 
        internal bool IsConnected => _isConnected;
 
        internal bool IsStreamOpen => _isStreamOpen;
 
        internal SmtpReplyReaderFactory? Reader => _responseReader;
 
        internal bool EnableSsl
        {
            get
            {
                return _enableSsl;
            }
            set
            {
                _enableSsl = value;
            }
        }
 
        internal X509CertificateCollection? ClientCertificates
        {
            get
            {
                return _clientCertificates;
            }
            set
            {
                _clientCertificates = value;
            }
        }
 
        internal void InitializeConnection(string host, int port)
        {
            _tcpClient!.Connect(host, port);
            _stream = _tcpClient.GetStream();
        }
 
        internal async Task InitializeConnectionAsync(string host, int port)
        {
            await _tcpClient!.ConnectAsync(host, port).ConfigureAwait(false);
            _stream = _tcpClient.GetStream();
        }
 
        internal async Task GetConnectionAsync<TIOAdapter>(string host, int port, CancellationToken cancellationToken = default)
            where TIOAdapter : IReadWriteAdapter
        {
            if (_isConnected)
            {
                throw new InvalidOperationException(SR.SmtpAlreadyConnected);
            }
 
            bool isAsync = typeof(TIOAdapter) == typeof(AsyncReadWriteAdapter);
 
            // Initialize the connection
            if (isAsync)
            {
                await InitializeConnectionAsync(host, port).ConfigureAwait(false);
            }
            else
            {
                InitializeConnection(host, port);
            }
 
            _responseReader = new SmtpReplyReaderFactory(_stream!);
 
            // Read the initial greeting
            LineInfo info = await _responseReader.GetNextReplyReader().ReadLineAsync<TIOAdapter>(cancellationToken).ConfigureAwait(false);
 
            switch (info.StatusCode)
            {
                case SmtpStatusCode.ServiceReady:
                    break;
                default:
                    throw new SmtpException(info.StatusCode, info.Line, true);
            }
 
            // Try EHLO first
            try
            {
                _extensions = await EHelloCommand.SendAsync<TIOAdapter>(this, _client!._clientDomain, cancellationToken).ConfigureAwait(false);
                ParseExtensions(_extensions);
            }
            catch (SmtpException e)
            {
                if ((e.StatusCode != SmtpStatusCode.CommandUnrecognized)
                    && (e.StatusCode != SmtpStatusCode.CommandNotImplemented))
                {
                    throw;
                }
 
                // Fall back to HELO if EHLO fails
                await HelloCommand.SendAsync<TIOAdapter>(this, _client!._clientDomain, cancellationToken).ConfigureAwait(false);
                // If ehello isn't supported, assume basic login
                _supportedAuth = SupportedAuth.Login;
            }
 
            // Handle SSL/TLS
            if (_enableSsl)
            {
                if (!_serverSupportsStartTls)
                {
                    // Either TLS is already established or server does not support TLS
                    if (!(_stream is SslStream))
                    {
                        throw new SmtpException(SR.MailServerDoesNotSupportStartTls);
                    }
                }
 
                await StartTlsCommand.SendAsync<TIOAdapter>(this, cancellationToken).ConfigureAwait(false);
 
#pragma warning disable SYSLIB0014 // ServicePointManager is obsolete
                SslStream sslStream = new SslStream(_stream!, false, ServicePointManager.ServerCertificateValidationCallback);
                if (isAsync)
                {
                    // If we are using async, we need to use the async version of AuthenticateAsClientAsync
                    await sslStream.AuthenticateAsClientAsync(
                        new SslClientAuthenticationOptions
                        {
                            TargetHost = host,
                            ClientCertificates = _clientCertificates,
                            EnabledSslProtocols = (SslProtocols)ServicePointManager.SecurityProtocol, // enums use same values
                            CertificateRevocationCheckMode = ServicePointManager.CheckCertificateRevocationList ?
                                X509RevocationMode.Online : X509RevocationMode.NoCheck,
                        },
                        cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    // Synchronous version
                    sslStream.AuthenticateAsClient(host, _clientCertificates, (SslProtocols)ServicePointManager.SecurityProtocol, ServicePointManager.CheckCertificateRevocationList);
                }
#pragma warning restore SYSLIB0014 // ServicePointManager is obsolete
 
                _stream = sslStream;
                _responseReader = new SmtpReplyReaderFactory(_stream);
 
                // According to RFC 3207: The client SHOULD send an EHLO command
                // as the first command after a successful TLS negotiation.
                _extensions = await EHelloCommand.SendAsync<TIOAdapter>(this, _client!._clientDomain, cancellationToken).ConfigureAwait(false);
                ParseExtensions(_extensions);
            }
 
            // If credentials were supplied, attempt authentication
            if (_credentials != null)
            {
                for (int i = 0; i < _authenticationModules.Length; i++)
                {
                    // Only authenticate if the auth protocol is supported
                    if (!AuthSupported(_authenticationModules[i]))
                    {
                        continue;
                    }
 
                    NetworkCredential? credential = _credentials.GetCredential(host, port, _authenticationModules[i].AuthenticationType);
                    if (credential == null)
                        continue;
 
                    Authorization? auth = SetContextAndTryAuthenticate(_authenticationModules[i], credential);
 
                    if (auth != null && auth.Message != null)
                    {
                        info = await AuthCommand.SendAsync<TIOAdapter>(this, _authenticationModules[i].AuthenticationType, auth.Message, cancellationToken).ConfigureAwait(false);
 
                        if (info.StatusCode == SmtpStatusCode.CommandParameterNotImplemented)
                        {
                            continue;
                        }
 
                        while ((int)info.StatusCode == 334)
                        {
                            auth = _authenticationModules[i].Authenticate(info.Line, null, this, _client!.TargetName, null);
                            if (auth == null)
                            {
                                throw new SmtpException(SR.SmtpAuthenticationFailed);
                            }
                            info = await AuthCommand.SendAsync<TIOAdapter>(this, auth.Message, cancellationToken).ConfigureAwait(false);
 
                            if ((int)info.StatusCode == 235)
                            {
                                _authenticationModules[i].CloseContext(this);
                                _isConnected = true;
                                return;
                            }
                        }
                    }
                }
            }
 
            _isConnected = true;
        }
 
        internal async Task FlushAsync<TIOAdapter>(CancellationToken cancellationToken = default) where TIOAdapter : IReadWriteAdapter
        {
            await TIOAdapter.WriteAsync(_stream!, _bufferBuilder.GetBuffer().AsMemory(0, _bufferBuilder.Length), cancellationToken).ConfigureAwait(false);
            _bufferBuilder.Reset();
        }
 
        private void ShutdownConnection(bool isAbort)
        {
            if (!_isClosed)
            {
                lock (this)
                {
                    if (!_isClosed && _tcpClient != null)
                    {
                        try
                        {
                            try
                            {
                                if (isAbort)
                                {
                                    // Must destroy manually since sending a QUIT here might not be
                                    // interpreted correctly by the server if it's in the middle of a
                                    // DATA command or some similar situation.  This may send a RST
                                    // but this is ok in this situation.  Do not reuse this connection
                                    _tcpClient.LingerState = new LingerOption(true, 0);
                                }
                                else
                                {
                                    // Gracefully close the transmission channel
                                    _tcpClient.Client.Blocking = false;
                                    QuitCommand.SendAsync<SyncReadWriteAdapter>(this).GetAwaiter().GetResult();
                                }
                            }
                            finally
                            {
                                //free cbt buffer
                                _stream?.Close();
                                _tcpClient.Dispose();
                            }
                        }
                        catch (IOException)
                        {
                            // Network failure
                        }
                        catch (ObjectDisposedException)
                        {
                            // See https://github.com/dotnet/runtime/issues/30732, and potentially
                            // catch additional exception types here if need demonstrates.
                        }
                    }
 
                    _isClosed = true;
                }
            }
 
            _isConnected = false;
        }
 
        internal void ReleaseConnection()
        {
            ShutdownConnection(false);
        }
 
        internal void Abort()
        {
            ShutdownConnection(true);
        }
 
        private Authorization? SetContextAndTryAuthenticate(ISmtpAuthenticationModule module, NetworkCredential? credential)
        {
            return module.Authenticate(null, credential, this, _client!.TargetName, null);
        }
 
        internal Stream GetClosableStream()
        {
            ClosableStream cs = new ClosableStream(_stream!, _onCloseHandler);
            _isStreamOpen = true;
            return cs;
        }
 
        private void OnClose(object? sender, EventArgs args)
        {
            _isStreamOpen = false;
 
            DataStopCommand.SendAsync<SyncReadWriteAdapter>(this).GetAwaiter().GetResult();
        }
    }
}