389 lines
15 KiB
C#
389 lines
15 KiB
C#
|
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;
|
|||
|
|
|||
|
/// <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
|
|||
|
{
|
|||
|
/// <summary>
|
|||
|
/// Initializes a new instance of the <see cref="SmtpClient" /> class.
|
|||
|
/// </summary>
|
|||
|
/// <param name="host">The host.</param>
|
|||
|
/// <param name="port">The port.</param>
|
|||
|
/// <exception cref="ArgumentNullException">host.</exception>
|
|||
|
public SmtpClient(string host, int port)
|
|||
|
{
|
|||
|
Host = host ?? throw new ArgumentNullException(nameof(host));
|
|||
|
Port = port;
|
|||
|
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 int 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 bool 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>
|
|||
|
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);
|
|||
|
}
|
|||
|
|
|||
|
/// <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 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 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();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|