Init...
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerates all of the well-known SMTP command names.
|
||||
/// </summary>
|
||||
public enum SmtpCommandNames
|
||||
{
|
||||
/// <summary>
|
||||
/// An unknown command
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// The helo command
|
||||
/// </summary>
|
||||
HELO,
|
||||
|
||||
/// <summary>
|
||||
/// The ehlo command
|
||||
/// </summary>
|
||||
EHLO,
|
||||
|
||||
/// <summary>
|
||||
/// The quit command
|
||||
/// </summary>
|
||||
QUIT,
|
||||
|
||||
/// <summary>
|
||||
/// The help command
|
||||
/// </summary>
|
||||
HELP,
|
||||
|
||||
/// <summary>
|
||||
/// The noop command
|
||||
/// </summary>
|
||||
NOOP,
|
||||
|
||||
/// <summary>
|
||||
/// The rset command
|
||||
/// </summary>
|
||||
RSET,
|
||||
|
||||
/// <summary>
|
||||
/// The mail command
|
||||
/// </summary>
|
||||
MAIL,
|
||||
|
||||
/// <summary>
|
||||
/// The data command
|
||||
/// </summary>
|
||||
DATA,
|
||||
|
||||
/// <summary>
|
||||
/// The send command
|
||||
/// </summary>
|
||||
SEND,
|
||||
|
||||
/// <summary>
|
||||
/// The soml command
|
||||
/// </summary>
|
||||
SOML,
|
||||
|
||||
/// <summary>
|
||||
/// The saml command
|
||||
/// </summary>
|
||||
SAML,
|
||||
|
||||
/// <summary>
|
||||
/// The RCPT command
|
||||
/// </summary>
|
||||
RCPT,
|
||||
|
||||
/// <summary>
|
||||
/// The vrfy command
|
||||
/// </summary>
|
||||
VRFY,
|
||||
|
||||
/// <summary>
|
||||
/// The expn command
|
||||
/// </summary>
|
||||
EXPN,
|
||||
|
||||
/// <summary>
|
||||
/// The starttls command
|
||||
/// </summary>
|
||||
STARTTLS,
|
||||
|
||||
/// <summary>
|
||||
/// The authentication command
|
||||
/// </summary>
|
||||
AUTH,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the reply code severities.
|
||||
/// </summary>
|
||||
public enum SmtpReplyCodeSeverities
|
||||
{
|
||||
/// <summary>
|
||||
/// The unknown severity
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The positive completion severity
|
||||
/// </summary>
|
||||
PositiveCompletion = 200,
|
||||
|
||||
/// <summary>
|
||||
/// The positive intermediate severity
|
||||
/// </summary>
|
||||
PositiveIntermediate = 300,
|
||||
|
||||
/// <summary>
|
||||
/// The transient negative severity
|
||||
/// </summary>
|
||||
TransientNegative = 400,
|
||||
|
||||
/// <summary>
|
||||
/// The permanent negative severity
|
||||
/// </summary>
|
||||
PermanentNegative = 500,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the reply code categories.
|
||||
/// </summary>
|
||||
public enum SmtpReplyCodeCategories
|
||||
{
|
||||
/// <summary>
|
||||
/// The unknown category
|
||||
/// </summary>
|
||||
Unknown = -1,
|
||||
|
||||
/// <summary>
|
||||
/// The syntax category
|
||||
/// </summary>
|
||||
Syntax = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The information category
|
||||
/// </summary>
|
||||
Information = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The connections category
|
||||
/// </summary>
|
||||
Connections = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The unspecified a category
|
||||
/// </summary>
|
||||
UnspecifiedA = 3,
|
||||
|
||||
/// <summary>
|
||||
/// The unspecified b category
|
||||
/// </summary>
|
||||
UnspecifiedB = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The system category
|
||||
/// </summary>
|
||||
System = 5,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains useful constants and definitions.
|
||||
/// </summary>
|
||||
public static class SmtpDefinitions
|
||||
{
|
||||
/// <summary>
|
||||
/// The string sequence that delimits the end of the DATA command.
|
||||
/// </summary>
|
||||
public const string SmtpDataCommandTerminator = "\r\n.\r\n";
|
||||
|
||||
/// <summary>
|
||||
/// Lists the AUTH methods supported by default.
|
||||
/// </summary>
|
||||
public static class SmtpAuthMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// The plain method.
|
||||
/// </summary>
|
||||
public const string Plain = "PLAIN";
|
||||
|
||||
/// <summary>
|
||||
/// The login method.
|
||||
/// </summary>
|
||||
public const string Login = "LOGIN";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
using Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Mail;
|
||||
|
||||
/// <summary>
|
||||
/// Use this class to store the sender session data.
|
||||
/// </summary>
|
||||
internal class SmtpSender
|
||||
{
|
||||
private readonly string _sessionId;
|
||||
private string _requestText;
|
||||
|
||||
public SmtpSender(string sessionId)
|
||||
{
|
||||
_sessionId = sessionId;
|
||||
}
|
||||
|
||||
public string RequestText
|
||||
{
|
||||
get => _requestText;
|
||||
set
|
||||
{
|
||||
_requestText = value;
|
||||
$" TX {_requestText}".Trace(typeof(SmtpClient), _sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
public string ReplyText { get; set; }
|
||||
|
||||
public bool IsReplyOk => ReplyText.StartsWith("250 ", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public void ValidateReply()
|
||||
{
|
||||
if (ReplyText == null)
|
||||
throw new SmtpException("There was no response from the server");
|
||||
|
||||
try
|
||||
{
|
||||
var response = SmtpServerReply.Parse(ReplyText);
|
||||
$" RX {ReplyText} - {response.IsPositive}".Trace(typeof(SmtpClient), _sessionId);
|
||||
|
||||
if (response.IsPositive) return;
|
||||
|
||||
var responseContent = response.Content.Any()
|
||||
? string.Join(";", response.Content.ToArray())
|
||||
: string.Empty;
|
||||
|
||||
throw new SmtpException((SmtpStatusCode)response.ReplyCode, responseContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!(ex is SmtpException))
|
||||
throw new SmtpException($"Could not parse server response: {ReplyText}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an SMTP server response object.
|
||||
/// </summary>
|
||||
public class SmtpServerReply
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </summary>
|
||||
/// <param name="responseCode">The response code.</param>
|
||||
/// <param name="statusCode">The status code.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
public SmtpServerReply(int responseCode, string statusCode, params string[] content)
|
||||
{
|
||||
Content = new List<string>();
|
||||
ReplyCode = responseCode;
|
||||
EnhancedStatusCode = statusCode;
|
||||
Content.AddRange(content);
|
||||
IsValid = responseCode >= 200 && responseCode < 600;
|
||||
ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
|
||||
ReplyCodeCategory = SmtpReplyCodeCategories.Unknown;
|
||||
|
||||
if (!IsValid) return;
|
||||
if (responseCode >= 200) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion;
|
||||
if (responseCode >= 300) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate;
|
||||
if (responseCode >= 400) ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative;
|
||||
if (responseCode >= 500) ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative;
|
||||
if (responseCode >= 600) ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
|
||||
|
||||
if (int.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out var middleDigit))
|
||||
{
|
||||
if (middleDigit >= 0 && middleDigit <= 5)
|
||||
ReplyCodeCategory = (SmtpReplyCodeCategories) middleDigit;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </summary>
|
||||
public SmtpServerReply()
|
||||
: this(0, string.Empty, string.Empty)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </summary>
|
||||
/// <param name="responseCode">The response code.</param>
|
||||
/// <param name="statusCode">The status code.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
public SmtpServerReply(int responseCode, string statusCode, string content)
|
||||
: this(responseCode, statusCode, new[] {content})
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </summary>
|
||||
/// <param name="responseCode">The response code.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
public SmtpServerReply(int responseCode, string content)
|
||||
: this(responseCode, string.Empty, content)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pre-built responses (https://tools.ietf.org/html/rfc5321#section-4.2.2)
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command unrecognized reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply CommandUnrecognized =>
|
||||
new SmtpServerReply(500, "Syntax error, command unrecognized");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the syntax error arguments reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply SyntaxErrorArguments =>
|
||||
new SmtpServerReply(501, "Syntax error in parameters or arguments");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command not implemented reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply CommandNotImplemented => new SmtpServerReply(502, "Command not implemented");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bad sequence of commands reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply BadSequenceOfCommands => new SmtpServerReply(503, "Bad sequence of commands");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the protocol violation reply.
|
||||
/// </summary>=
|
||||
public static SmtpServerReply ProtocolViolation =>
|
||||
new SmtpServerReply(451, "Requested action aborted: error in processing");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system status bye reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply SystemStatusBye =>
|
||||
new SmtpServerReply(221, "Service closing transmission channel");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system status help reply.
|
||||
/// </summary>=
|
||||
public static SmtpServerReply SystemStatusHelp => new SmtpServerReply(221, "Refer to RFC 5321");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bad syntax command empty reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply BadSyntaxCommandEmpty => new SmtpServerReply(400, "Error: bad syntax");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OK reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply Ok => new SmtpServerReply(250, "OK");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization required reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply AuthorizationRequired => new SmtpServerReply(530, "Authorization Required");
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response severity.
|
||||
/// </summary>
|
||||
public SmtpReplyCodeSeverities ReplyCodeSeverity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response category.
|
||||
/// </summary>
|
||||
public SmtpReplyCodeCategories ReplyCodeCategory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the numeric response code.
|
||||
/// </summary>
|
||||
public int ReplyCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the enhanced status code.
|
||||
/// </summary>
|
||||
public string EnhancedStatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content.
|
||||
/// </summary>
|
||||
public List<string> Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the response code is between 200 and 599.
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is positive.
|
||||
/// </summary>
|
||||
public bool IsPositive => ReplyCode >= 200 && ReplyCode <= 399;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified text into a Server Reply for thorough analysis.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <returns>A new instance of SMTP server response object.</returns>
|
||||
public static SmtpServerReply Parse(string text)
|
||||
{
|
||||
var lines = text.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lines.Length == 0) return new SmtpServerReply();
|
||||
|
||||
var lastLineParts = lines.Last().Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries);
|
||||
var enhancedStatusCode = string.Empty;
|
||||
int.TryParse(lastLineParts[0], out var responseCode);
|
||||
if (lastLineParts.Length > 1)
|
||||
{
|
||||
if (lastLineParts[1].Split('.').Length == 3)
|
||||
enhancedStatusCode = lastLineParts[1];
|
||||
}
|
||||
|
||||
var content = new List<string>();
|
||||
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var splitChar = i == lines.Length - 1 ? " " : "-";
|
||||
|
||||
var lineParts = lines[i].Split(new[] {splitChar}, 2, StringSplitOptions.None);
|
||||
var lineContent = lineParts.Last();
|
||||
if (string.IsNullOrWhiteSpace(enhancedStatusCode) == false)
|
||||
lineContent = lineContent.Replace(enhancedStatusCode, string.Empty).Trim();
|
||||
|
||||
content.Add(lineContent);
|
||||
}
|
||||
|
||||
return new SmtpServerReply(responseCode, enhancedStatusCode, content.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents this instance.
|
||||
/// </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
var responseCodeText = ReplyCode.ToString(CultureInfo.InvariantCulture);
|
||||
var statusCodeText = string.IsNullOrWhiteSpace(EnhancedStatusCode)
|
||||
? string.Empty
|
||||
: $" {EnhancedStatusCode.Trim()}";
|
||||
if (Content.Count == 0) return $"{responseCodeText}{statusCodeText}";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
for (var i = 0; i < Content.Count; i++)
|
||||
{
|
||||
var isLastLine = i == Content.Count - 1;
|
||||
|
||||
builder.Append(isLastLine
|
||||
? $"{responseCodeText}{statusCodeText} {Content[i]}"
|
||||
: $"{responseCodeText}-{Content[i]}\r\n");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of an SMTP session associated with a client.
|
||||
/// </summary>
|
||||
public class SmtpSessionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpSessionState"/> class.
|
||||
/// </summary>
|
||||
public SmtpSessionState()
|
||||
{
|
||||
DataBuffer = new List<byte>();
|
||||
Reset(true);
|
||||
ResetAuthentication();
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the contents of the data buffer.
|
||||
/// </summary>
|
||||
public List<byte> DataBuffer { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance has initiated.
|
||||
/// </summary>
|
||||
public bool HasInitiated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the current session supports extensions.
|
||||
/// </summary>
|
||||
public bool SupportsExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client hostname.
|
||||
/// </summary>
|
||||
public string ClientHostname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the session is currently receiving DATA.
|
||||
/// </summary>
|
||||
public bool IsInDataMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sender address.
|
||||
/// </summary>
|
||||
public string SenderAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recipients.
|
||||
/// </summary>
|
||||
public List<string> Recipients { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extended data supporting any additional field for storage by a responder implementation.
|
||||
/// </summary>
|
||||
public object ExtendedData { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region AUTH State
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is in authentication mode.
|
||||
/// </summary>
|
||||
public bool IsInAuthMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has provided username.
|
||||
/// </summary>
|
||||
public bool HasProvidedUsername => string.IsNullOrWhiteSpace(Username) == false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is authenticated.
|
||||
/// </summary>
|
||||
public bool IsAuthenticated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication mode.
|
||||
/// </summary>
|
||||
public string AuthMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is channel secure.
|
||||
/// </summary>
|
||||
public bool IsChannelSecure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resets the authentication state.
|
||||
/// </summary>
|
||||
public void ResetAuthentication()
|
||||
{
|
||||
Username = string.Empty;
|
||||
Password = string.Empty;
|
||||
AuthMode = string.Empty;
|
||||
IsInAuthMode = false;
|
||||
IsAuthenticated = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Resets the data mode to false, clears the recipients, the sender address and the data buffer.
|
||||
/// </summary>
|
||||
public void ResetEmail()
|
||||
{
|
||||
IsInDataMode = false;
|
||||
Recipients.Clear();
|
||||
SenderAddress = string.Empty;
|
||||
DataBuffer.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the state table entirely.
|
||||
/// </summary>
|
||||
/// <param name="clearExtensionData">if set to <c>true</c> [clear extension data].</param>
|
||||
public void Reset(bool clearExtensionData)
|
||||
{
|
||||
HasInitiated = false;
|
||||
SupportsExtensions = false;
|
||||
ClientHostname = string.Empty;
|
||||
ResetEmail();
|
||||
|
||||
if (clearExtensionData)
|
||||
ExtendedData = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new object that is a copy of the current instance.
|
||||
/// </summary>
|
||||
/// <returns>A clone.</returns>
|
||||
public virtual SmtpSessionState Clone()
|
||||
{
|
||||
var clonedState = this.CopyPropertiesToNew<SmtpSessionState>(new[] {nameof(DataBuffer)});
|
||||
clonedState.DataBuffer.AddRange(DataBuffer);
|
||||
clonedState.Recipients.AddRange(Recipients);
|
||||
|
||||
return clonedState;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user