Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.NotificationService/SmtpErrorClassifier.cs
T
Joseph Doherty 9cff87fe85 docs(comments): strip internal task/milestone/bundle bookkeeping from code comments
Remove project bookkeeping citations from shipped code comments across the
solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/
issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels,
and C/D/K/S/T phase labels.

Comment text only — no code logic, string/log literals, or XML-doc structure
changed. Genuine descriptions are preserved (only the citation is stripped),
and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365,
UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker
TaskReferenceInComment / TrackingReferenceInComment checks plus targeted
grep passes; full solution builds clean, append-only guard tests pass.
2026-07-07 11:03:26 -04:00

98 lines
3.7 KiB
C#

using System.Net.Sockets;
using MailKit;
using MailKit.Net.Smtp;
namespace ZB.MOM.WW.ScadaBridge.NotificationService;
/// <summary>
/// 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.
/// </summary>
public enum SmtpErrorClass
{
/// <summary>Cancellation or an unrecognised exception — caller decides.</summary>
Unknown,
/// <summary>Retryable failure (4xx, connection/socket/protocol error, timeout).</summary>
Transient,
/// <summary>Non-retryable failure (5xx) — must not be retried.</summary>
Permanent,
}
/// <summary>
/// Classifies an SMTP failure using MailKit's typed exceptions and
/// the numeric <see cref="SmtpStatusCode"/> rather than locale-dependent substring
/// matching on the exception message.
/// <para>
/// Public and shared: the central Notification Outbox's <c>EmailNotificationDeliveryAdapter</c>
/// routes every SMTP failure through this single policy. (The orphaned site-side
/// <c>NotificationDeliveryService</c> that previously co-used this classifier was removed
/// when sites stopped delivering notifications.)
/// </para>
/// </summary>
public static class SmtpErrorClassifier
{
/// <summary>
/// Classifies an SMTP failure. A cancellation requested by the caller is never
/// treated as a transient SMTP error.
/// </summary>
/// <param name="ex">The exception thrown by the SMTP send sequence.</param>
/// <param name="cancellationToken">
/// The token governing the send; a requested cancellation classifies as
/// <see cref="SmtpErrorClass.Unknown"/> so the caller can re-throw it.
/// </param>
/// <returns>The <see cref="SmtpErrorClass"/> describing whether the failure is transient, permanent, or unknown.</returns>
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;
}
/// <summary>
/// Convenience predicate: true when <see cref="Classify"/> returns
/// <see cref="SmtpErrorClass.Transient"/>.
/// </summary>
/// <param name="ex">The exception to classify.</param>
/// <param name="cancellationToken">Cancellation token passed to <see cref="Classify"/>.</param>
/// <returns><c>true</c> when <see cref="Classify"/> returns <see cref="SmtpErrorClass.Transient"/>.</returns>
public static bool IsTransient(Exception ex, CancellationToken cancellationToken)
=> Classify(ex, cancellationToken) == SmtpErrorClass.Transient;
}