using System.Net.Sockets;
using MailKit;
using MailKit.Net.Smtp;
namespace ScadaLink.NotificationService;
///
/// NS-002/NS-003: The classification of an SMTP delivery failure. This decides
/// whether a failure is retried or surfaced to the caller, so it is part of the
/// system's correctness-relevant behaviour.
///
public enum SmtpErrorClass
{
/// Cancellation or an unrecognised exception — caller decides.
Unknown,
/// Retryable failure (4xx, connection/socket/protocol error, timeout).
Transient,
/// Non-retryable failure (5xx) — must not be retried.
Permanent,
}
///
/// NS-002/NS-003: Classifies an SMTP failure using MailKit's typed exceptions and
/// the numeric rather than locale-dependent substring
/// matching on the exception message.
///
/// Public and shared: both (store-and-forward
/// delivery) and the central Notification Outbox's EmailNotificationDeliveryAdapter
/// route every SMTP failure through this single policy, so a transient/permanent
/// boundary change cannot diverge between the two delivery paths.
///
///
public static class SmtpErrorClassifier
{
///
/// Classifies an SMTP failure. A cancellation requested by the caller is never
/// treated as a transient SMTP error.
///
/// The exception thrown by the SMTP send sequence.
///
/// The token governing the send; a requested cancellation classifies as
/// so the caller can re-throw it.
///
public static SmtpErrorClass Classify(Exception ex, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(ex);
// A deliberate cancellation is not an SMTP error at all.
if (ex is OperationCanceledException && cancellationToken.IsCancellationRequested)
{
return SmtpErrorClass.Unknown;
}
// MailKit reports SMTP command failures with the real status code; the
// SmtpStatusCode enum's underlying value is the numeric SMTP reply code.
if (ex is SmtpCommandException command)
{
var code = (int)command.StatusCode;
if (code >= 400 && code < 500)
{
return SmtpErrorClass.Transient;
}
if (code >= 500 && code < 600)
{
return SmtpErrorClass.Permanent;
}
return SmtpErrorClass.Unknown;
}
// Protocol errors, a dropped/unavailable service, socket failures and
// timeouts are all retryable — the message has not been rejected.
if (ex is SmtpProtocolException
or ServiceNotConnectedException
or SocketException
or TimeoutException)
{
return SmtpErrorClass.Transient;
}
return SmtpErrorClass.Unknown;
}
///
/// Convenience predicate: true when returns
/// .
///
public static bool IsTransient(Exception ex, CancellationToken cancellationToken)
=> Classify(ex, cancellationToken) == SmtpErrorClass.Transient;
}