2019-12-08 21:23:54 +01:00
#nullable enable
using System.Threading ;
using System ;
using System.Linq ;
using System.Net ;
using System.Net.Sockets ;
using System.Security ;
using System.Text ;
using System.Net.Security ;
using System.Threading.Tasks ;
using System.Collections.Generic ;
using System.Net.Mail ;
namespace Swan.Net.Smtp {
/// <summary>
/// Represents a basic SMTP client that is capable of submitting messages to an SMTP server.
/// </summary>
/// <example>
/// The following code explains how to send a simple e-mail.
/// <code>
/// using System.Net.Mail;
///
/// class Example
/// {
/// static void Main()
/// {
/// // create a new smtp client using google's smtp server
/// var client = new Swan.Net.Smtp.SmtpClient("smtp.gmail.com", 587);
///
/// // send an email
/// client.SendMailAsync(
/// new MailMessage("sender@test.com", "recipient@test.cm", "Subject", "Body"));
/// }
/// }
/// </code>
///
/// The following code demonstrates how to sent an e-mail using a SmtpSessionState:
/// <code>
/// using Swan.Net.Smtp;
///
/// class Example
/// {
/// static void Main()
/// {
/// // create a new smtp client using google's smtp server
/// var client = new SmtpClient("smtp.gmail.com", 587);
///
/// // create a new session state with a sender address
/// var session = new SmtpSessionState { SenderAddress = "sender@test.com" };
///
/// // add a recipient
/// session.Recipients.Add("recipient@test.cm");
///
/// // send
/// client.SendMailAsync(session);
/// }
/// }
/// </code>
///
/// The following code shows how to send an e-mail with an attachment using MimeKit:
/// <code>
/// using MimeKit;
/// using Swan.Net.Smtp;
///
/// class Example
/// {
/// static void Main()
/// {
/// // create a new smtp client using google's smtp server
/// var client = new SmtpClient("smtp.gmail.com", 587);
///
/// // create a new session state with a sender address
/// var session = new SmtpSessionState { SenderAddress = "sender@test.com" };
///
/// // add a recipient
/// session.Recipients.Add("recipient@test.cm");
///
/// // load a file as an attachment
/// var attachment = new MimePart("image", "gif")
/// {
/// Content = new
/// MimeContent(File.OpenRead("meme.gif"), ContentEncoding.Default),
/// ContentDisposition =
/// new ContentDisposition(ContentDisposition.Attachment),
/// ContentTransferEncoding = ContentEncoding.Base64,
/// FileName = Path.GetFileName("meme.gif")
/// };
///
/// // send
/// client.SendMailAsync(session);
/// }
/// }
/// </code>
/// </example>
public class SmtpClient {
2019-12-04 18:57:18 +01:00
/// <summary>
2019-12-08 21:23:54 +01:00
/// Initializes a new instance of the <see cref="SmtpClient" /> class.
2019-12-04 18:57:18 +01:00
/// </summary>
2019-12-08 21:23:54 +01:00
/// <param name="host">The host.</param>
/// <param name="port">The port.</param>
/// <exception cref="ArgumentNullException">host.</exception>
public SmtpClient ( String host , Int32 port ) {
this . Host = host ? ? throw new ArgumentNullException ( nameof ( host ) ) ;
this . Port = port ;
this . ClientHostname = Network . HostName ;
}
/// <summary>
/// Gets or sets the credentials. No credentials will be used if set to null.
/// </summary>
/// <value>
/// The credentials.
/// </value>
public NetworkCredential ? Credentials {
get ; set ;
}
/// <summary>
/// Gets the host.
/// </summary>
/// <value>
/// The host.
/// </value>
public String Host {
get ;
}
/// <summary>
/// Gets the port.
/// </summary>
/// <value>
/// The port.
/// </value>
public Int32 Port {
get ;
}
/// <summary>
/// Gets or sets a value indicating whether the SSL is enabled.
/// If set to false, communication between client and server will not be secured.
/// </summary>
/// <value>
/// <c>true</c> if [enable SSL]; otherwise, <c>false</c>.
/// </value>
public Boolean EnableSsl {
get ; set ;
}
/// <summary>
/// Gets or sets the name of the client that gets announced to the server.
/// </summary>
/// <value>
/// The client hostname.
/// </value>
public String ClientHostname {
get ; set ;
}
/// <summary>
/// Sends an email message asynchronously.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="callback">The callback.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task that represents the asynchronous of send email operation.
/// </returns>
/// <exception cref="ArgumentNullException">message.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
public Task SendMailAsync ( MailMessage message , String ? sessionId = null , RemoteCertificateValidationCallback ? callback = null , CancellationToken cancellationToken = default ) {
if ( message = = null ) {
throw new ArgumentNullException ( nameof ( message ) ) ;
}
SmtpSessionState state = new SmtpSessionState {
AuthMode = this . Credentials = = null ? String . Empty : SmtpDefinitions . SmtpAuthMethods . Login ,
ClientHostname = ClientHostname ,
IsChannelSecure = EnableSsl ,
SenderAddress = message . From . Address ,
} ;
if ( this . Credentials ! = null ) {
state . Username = this . Credentials . UserName ;
state . Password = this . Credentials . Password ;
}
foreach ( MailAddress recipient in message . To ) {
state . Recipients . Add ( recipient . Address ) ;
}
state . DataBuffer . AddRange ( message . ToMimeMessage ( ) . ToArray ( ) ) ;
return this . SendMailAsync ( state , sessionId , callback , cancellationToken ) ;
}
/// <summary>
/// Sends an email message using a session state object.
/// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
/// rather from the properties of this class.
/// </summary>
/// <param name="sessionState">The state.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="callback">The callback.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task that represents the asynchronous of send email operation.
/// </returns>
/// <exception cref="ArgumentNullException">sessionState.</exception>
public Task SendMailAsync ( SmtpSessionState sessionState , String ? sessionId = null , RemoteCertificateValidationCallback ? callback = null , CancellationToken cancellationToken = default ) {
if ( sessionState = = null ) {
throw new ArgumentNullException ( nameof ( sessionState ) ) ;
}
return this . SendMailAsync ( new [ ] { sessionState } , sessionId , callback , cancellationToken ) ;
}
/// <summary>
/// Sends an array of email messages using a session state object.
/// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
/// rather from the properties of this class.
/// </summary>
/// <param name="sessionStates">The session states.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="callback">The callback.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task that represents the asynchronous of send email operation.
/// </returns>
/// <exception cref="ArgumentNullException">sessionStates.</exception>
/// <exception cref="SecurityException">Could not upgrade the channel to SSL.</exception>
/// <exception cref="SmtpException">Defines an SMTP Exceptions class.</exception>
public async Task SendMailAsync ( IEnumerable < SmtpSessionState > sessionStates , String ? sessionId = null , RemoteCertificateValidationCallback ? callback = null , CancellationToken cancellationToken = default ) {
if ( sessionStates = = null ) {
throw new ArgumentNullException ( nameof ( sessionStates ) ) ;
}
using TcpClient tcpClient = new TcpClient ( ) ;
await tcpClient . ConnectAsync ( this . Host , this . Port ) . ConfigureAwait ( false ) ;
using Connection connection = new Connection ( tcpClient , Encoding . UTF8 , "\r\n" , true , 1000 ) ;
SmtpSender sender = new SmtpSender ( sessionId ) ;
try {
// Read the greeting message
sender . ReplyText = await connection . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
// EHLO 1
await this . SendEhlo ( sender , connection , cancellationToken ) . ConfigureAwait ( false ) ;
// STARTTLS
if ( this . EnableSsl ) {
sender . RequestText = $"{SmtpCommandNames.STARTTLS}" ;
await connection . WriteLineAsync ( sender . RequestText , cancellationToken ) . ConfigureAwait ( false ) ;
sender . ReplyText = await connection . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
sender . ValidateReply ( ) ;
if ( await connection . UpgradeToSecureAsClientAsync ( callback : callback ) . ConfigureAwait ( false ) = = false ) {
throw new SecurityException ( "Could not upgrade the channel to SSL." ) ;
}
}
// EHLO 2
await this . SendEhlo ( sender , connection , cancellationToken ) . ConfigureAwait ( false ) ;
// AUTH
if ( this . Credentials ! = null ) {
ConnectionAuth auth = new ConnectionAuth ( connection , sender , this . Credentials ) ;
await auth . AuthenticateAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
}
foreach ( SmtpSessionState sessionState in sessionStates ) {
{
// MAIL FROM
sender . RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>" ;
await connection . WriteLineAsync ( sender . RequestText , cancellationToken ) . ConfigureAwait ( false ) ;
sender . ReplyText = await connection . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
sender . ValidateReply ( ) ;
}
// RCPT TO
foreach ( String recipient in sessionState . Recipients ) {
sender . RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>" ;
await connection . WriteLineAsync ( sender . RequestText , cancellationToken ) . ConfigureAwait ( false ) ;
sender . ReplyText = await connection . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
sender . ValidateReply ( ) ;
}
{
// DATA
sender . RequestText = $"{SmtpCommandNames.DATA}" ;
await connection . WriteLineAsync ( sender . RequestText , cancellationToken ) . ConfigureAwait ( false ) ;
sender . ReplyText = await connection . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
sender . ValidateReply ( ) ;
}
{
// CONTENT
String dataTerminator = sessionState . DataBuffer . Skip ( sessionState . DataBuffer . Count - 5 ) . ToText ( ) ;
sender . RequestText = $"Buffer ({sessionState.DataBuffer.Count} bytes)" ;
await connection . WriteDataAsync ( sessionState . DataBuffer . ToArray ( ) , true , cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! dataTerminator . EndsWith ( SmtpDefinitions . SmtpDataCommandTerminator ) ) {
await connection . WriteTextAsync ( SmtpDefinitions . SmtpDataCommandTerminator , cancellationToken ) . ConfigureAwait ( false ) ;
}
sender . ReplyText = await connection . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
sender . ValidateReply ( ) ;
}
}
{
// QUIT
sender . RequestText = $"{SmtpCommandNames.QUIT}" ;
await connection . WriteLineAsync ( sender . RequestText , cancellationToken ) . ConfigureAwait ( false ) ;
sender . ReplyText = await connection . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
sender . ValidateReply ( ) ;
}
} catch ( Exception ex ) {
throw new SmtpException ( $"Could not send email - Session ID {sessionId}. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}" ) ;
}
}
private async Task SendEhlo ( SmtpSender sender , Connection connection , CancellationToken cancellationToken ) {
sender . RequestText = $"{SmtpCommandNames.EHLO} {this.ClientHostname}" ;
await connection . WriteLineAsync ( sender . RequestText , cancellationToken ) . ConfigureAwait ( false ) ;
do {
sender . ReplyText = await connection . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
}
while ( ! sender . IsReplyOk ) ;
sender . ValidateReply ( ) ;
}
private class ConnectionAuth {
private readonly SmtpSender _sender ;
private readonly Connection _connection ;
private readonly NetworkCredential _credentials ;
public ConnectionAuth ( Connection connection , SmtpSender sender , NetworkCredential credentials ) {
this . _connection = connection ;
this . _sender = sender ;
this . _credentials = credentials ;
}
public async Task AuthenticateAsync ( CancellationToken ct ) {
this . _sender . RequestText = $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.UserName))}" ;
await this . _connection . WriteLineAsync ( this . _sender . RequestText , ct ) . ConfigureAwait ( false ) ;
this . _sender . ReplyText = await this . _connection . ReadLineAsync ( ct ) . ConfigureAwait ( false ) ;
this . _sender . ValidateReply ( ) ;
this . _sender . RequestText = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( this . _credentials . Password ) ) ;
await this . _connection . WriteLineAsync ( this . _sender . RequestText , ct ) . ConfigureAwait ( false ) ;
this . _sender . ReplyText = await this . _connection . ReadLineAsync ( ct ) . ConfigureAwait ( false ) ;
this . _sender . ValidateReply ( ) ;
}
}
}
2019-12-04 18:57:18 +01:00
}