FU1 of the Notification Outbox follow-ups. EmailNotificationDeliveryAdapter carried verbatim private copies of credential redaction, SMTP error classification, and address validation because the NotificationService helpers were internal. This eliminates the divergence risk by promoting the helpers to public and deleting the adapter's copies. - CredentialRedactor: internal -> public. - Extract SmtpErrorClassifier + SmtpErrorClass enum into a new public static class; NotificationDeliveryService now routes classification through it (behavior unchanged). Adds focused SmtpErrorClassifierTests. - NotificationDeliveryService.ValidateAddresses: internal -> public; the adapter calls it directly. - EmailNotificationDeliveryAdapter: deleted ScrubCredentials, ClassifySmtpError, SmtpErrorClass, IsTransientSmtpError and ValidateAddresses copies. No InternalsVisibleTo hack — specific helpers promoted to public. Both test suites green; full solution builds clean.
53 lines
2.0 KiB
C#
53 lines
2.0 KiB
C#
namespace ScadaLink.NotificationService;
|
|
|
|
/// <summary>
|
|
/// NS-009: Scrubs SMTP credential secrets out of free text (typically exception
|
|
/// messages echoed back by an SMTP server) before that text is written to a log.
|
|
/// MailKit authentication exceptions can contain server responses that quote the
|
|
/// supplied credentials; this prevents a password, client secret, or OAuth2 token
|
|
/// from leaking into the operational logs.
|
|
/// <para>
|
|
/// Public so the central Notification Outbox's <c>EmailNotificationDeliveryAdapter</c>
|
|
/// can share this exact redaction logic rather than carry a divergent copy.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class CredentialRedactor
|
|
{
|
|
private const string Mask = "***REDACTED***";
|
|
|
|
/// <summary>
|
|
/// Returns <paramref name="text"/> with every secret component of the supplied
|
|
/// colon-delimited credential string masked.
|
|
/// </summary>
|
|
/// <param name="text">The text to scrub (e.g. an exception message).</param>
|
|
/// <param name="credentials">
|
|
/// The credential string in use — Basic Auth <c>user:pass</c> or OAuth2
|
|
/// <c>tenantId:clientId:clientSecret</c>. May be null.
|
|
/// </param>
|
|
public static string Scrub(string? text, string? credentials)
|
|
{
|
|
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(credentials))
|
|
{
|
|
return text ?? string.Empty;
|
|
}
|
|
|
|
var result = text;
|
|
|
|
// Mask each individual colon-delimited component (covers user, password,
|
|
// tenant, clientId, clientSecret) and the whole packed string. Order longest
|
|
// first so a component that is a substring of another is still fully masked.
|
|
var parts = credentials.Split(':')
|
|
.Where(p => p.Length >= 4)
|
|
.Append(credentials)
|
|
.Distinct()
|
|
.OrderByDescending(p => p.Length);
|
|
|
|
foreach (var part in parts)
|
|
{
|
|
result = result.Replace(part, Mask, StringComparison.Ordinal);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|