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; #if !NETSTANDARD1_3 using System.Net.Mail; #else using Exceptions; #endif namespace Unosquare.Swan.Networking { /// /// 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 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: /// /// 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 System.Net.Mail; /// /// 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; } #if !NETSTANDARD1_3 /// /// Sends an email message asynchronously. /// /// The message. /// The session identifier. /// The cancellation token. /// The callback. /// /// 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, CancellationToken ct = default, RemoteCertificateValidationCallback callback = null) { 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, ct, callback); } #endif /// /// 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 cancellation token. /// The callback. /// /// A task that represents the asynchronous of send email operation. /// /// sessionState. public Task SendMailAsync( SmtpSessionState sessionState, String sessionId = null, CancellationToken ct = default, RemoteCertificateValidationCallback callback = null) { if(sessionState == null) { throw new ArgumentNullException(nameof(sessionState)); } return this.SendMailAsync(new[] { sessionState }, sessionId, ct, callback); } /// /// 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 cancellation token. /// The callback. /// /// 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, CancellationToken ct = default, RemoteCertificateValidationCallback callback = null) { 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(ct).ConfigureAwait(false); // EHLO 1 await this.SendEhlo(ct, sender, connection).ConfigureAwait(false); // STARTTLS if(this.EnableSsl) { sender.RequestText = $"{SmtpCommandNames.STARTTLS}"; await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); sender.ReplyText = await connection.ReadLineAsync(ct).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(ct, sender, connection).ConfigureAwait(false); // AUTH if(this.Credentials != null) { ConnectionAuth auth = new ConnectionAuth(connection, sender, this.Credentials); await auth.AuthenticateAsync(ct).ConfigureAwait(false); } foreach(SmtpSessionState sessionState in sessionStates) { { // MAIL FROM sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>"; await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); sender.ValidateReply(); } // RCPT TO foreach(String recipient in sessionState.Recipients) { sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>"; await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); sender.ValidateReply(); } { // DATA sender.RequestText = $"{SmtpCommandNames.DATA}"; await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); sender.ReplyText = await connection.ReadLineAsync(ct).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, ct).ConfigureAwait(false); if(dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator) == false) { await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, ct).ConfigureAwait(false); } sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); sender.ValidateReply(); } } { // QUIT sender.RequestText = $"{SmtpCommandNames.QUIT}"; await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); sender.ValidateReply(); } } catch(Exception ex) { String errorMessage = $"Could not send email. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}"; errorMessage.Error(typeof(SmtpClient).FullName, sessionId); throw new SmtpException(errorMessage); } } } } private async Task SendEhlo(CancellationToken ct, SmtpSender sender, Connection connection) { sender.RequestText = $"{SmtpCommandNames.EHLO} {this.ClientHostname}"; await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); do { sender.ReplyText = await connection.ReadLineAsync(ct).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(); } } } }