namespace Swan.Net.Smtp { 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; /// /// 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, int port) { Host = host ?? throw new ArgumentNullException(nameof(host)); Port = port; 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 int 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 bool 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. public Task SendMailAsync( MailMessage message, string? sessionId = null, RemoteCertificateValidationCallback? callback = null, CancellationToken cancellationToken = default) { if (message == null) throw new ArgumentNullException(nameof(message)); var state = new SmtpSessionState { AuthMode = Credentials == null ? string.Empty : SmtpDefinitions.SmtpAuthMethods.Login, ClientHostname = ClientHostname, IsChannelSecure = EnableSsl, SenderAddress = message.From.Address, }; if (Credentials != null) { state.Username = Credentials.UserName; state.Password = Credentials.Password; } foreach (var recipient in message.To) { state.Recipients.Add(recipient.Address); } state.DataBuffer.AddRange(message.ToMimeMessage().ToArray()); return 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 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 var tcpClient = new TcpClient(); await tcpClient.ConnectAsync(Host, Port).ConfigureAwait(false); using var connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000); var sender = new SmtpSender(sessionId); try { // Read the greeting message sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); // EHLO 1 await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false); // STARTTLS if (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 SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false); // AUTH if (Credentials != null) { var auth = new ConnectionAuth(connection, sender, Credentials); await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false); } foreach (var 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 (var 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 var 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} {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) { _connection = connection; _sender = sender; _credentials = credentials; } public async Task AuthenticateAsync(CancellationToken ct) { _sender.RequestText = $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.UserName))}"; await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false); _sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false); _sender.ValidateReply(); _sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.Password)); await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false); _sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false); _sender.ValidateReply(); } } } }