#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 {
///
/// Represents a basic SMTP client that is capable of submitting messages to an SMTP server.
///
///
/// The following code explains how to send a simple e-mail.
///
/// 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"));
/// }
/// }
///
///
/// The following code demonstrates how to sent an e-mail using a SmtpSessionState:
///
/// 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);
/// }
/// }
///
///
/// The following code shows how to send an e-mail with an attachment using MimeKit:
///
/// 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);
/// }
/// }
///
///
public class SmtpClient {
///
/// Initializes a new instance of the class.
///
/// The host.
/// The port.
/// host.
public SmtpClient(String host, Int32 port) {
this.Host = host ?? throw new ArgumentNullException(nameof(host));
this.Port = port;
this.ClientHostname = Network.HostName;
}
///
/// Gets or sets the credentials. No credentials will be used if set to null.
///
///
/// The credentials.
///
public NetworkCredential? Credentials {
get; set;
}
///
/// Gets the host.
///
///
/// The host.
///
public String Host {
get;
}
///
/// Gets the port.
///
///
/// The port.
///
public Int32 Port {
get;
}
///
/// Gets or sets a value indicating whether the SSL is enabled.
/// If set to false, communication between client and server will not be secured.
///
///
/// true if [enable SSL]; otherwise, false.
///
public Boolean EnableSsl {
get; set;
}
///
/// Gets or sets the name of the client that gets announced to the server.
///
///
/// The client hostname.
///
public String ClientHostname {
get; set;
}
///
/// Sends an email message asynchronously.
///
/// The message.
/// The session identifier.
/// The callback.
/// The cancellation token.
///
/// A task that represents the asynchronous of send email operation.
///
/// message.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")]
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);
}
///
/// 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.
///
/// The state.
/// The session identifier.
/// The callback.
/// The cancellation token.
///
/// A task that represents the asynchronous of send email operation.
///
/// sessionState.
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);
}
///
/// 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.
///
/// The session states.
/// The session identifier.
/// The callback.
/// The cancellation token.
///
/// A task that represents the asynchronous of send email operation.
///
/// sessionStates.
/// Could not upgrade the channel to SSL.
/// Defines an SMTP Exceptions class.
public async Task SendMailAsync(IEnumerable 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();
}
}
}
}