< Summary

Information
Class: ArturRios.Util.Text.EmailAddress
Assembly: ArturRios.Util
File(s): /home/runner/work/dotnet-util/dotnet-util/src/Text/EmailAddress.cs
Line coverage
100%
Covered lines: 21
Uncovered lines: 0
Coverable lines: 21
Total lines: 101
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
Branch coverage: 100%
Method coverage
100%
Covered methods: 4
Fully covered methods: 4
Total methods: 4
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
TryNormalize(...)100%88100%
IsValid(...)100%11100%
NormalizeDomain(...)100%22100%

File(s)

/home/runner/work/dotnet-util/dotnet-util/src/Text/EmailAddress.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Globalization;
 3using ArturRios.Util.RegularExpressions;
 4
 5namespace ArturRios.Util.Text;
 6
 7/// <summary>
 8/// Normalization and validation for email addresses, layered on top of
 9/// <see cref="RegexCollection.EmailPattern"/>.
 10/// </summary>
 11/// <remarks>
 12/// <para>
 13/// Validating an address happens at three levels, and this type covers the first two. The pattern in
 14/// <see cref="RegexCollection"/> answers whether the text is shaped like an address. This type additionally
 15/// normalizes it, so that two spellings of one mailbox compare equal. Whether the mailbox actually exists
 16/// can only be settled by an MX lookup or a confirmation email.
 17/// </para>
 18/// <para>
 19/// <see cref="System.Net.Mail.MailAddress"/> is deliberately not used here. It is a parser rather than a
 20/// validator and accepts a great deal the pattern rejects, including "user@-hostname.com",
 21/// "user@hostname." and display-name forms such as "John Doe &lt;user@host.com&gt;".
 22/// </para>
 23/// </remarks>
 24public static class EmailAddress
 25{
 126    private static readonly IdnMapping Idn = new();
 27
 28    /// <summary>
 29    /// Validates <paramref name="value"/> and rewrites it into a canonical form.
 30    /// </summary>
 31    /// <param name="value">The address to normalize.</param>
 32    /// <param name="normalized">
 33    /// The canonical address when the method returns <see langword="true"/>, otherwise <see langword="null"/>.
 34    /// The domain is lowercased and, when internationalized, converted to its punycode form; the local part
 35    /// is left exactly as given, because it is case sensitive per RFC 5321.
 36    /// </param>
 37    /// <returns><see langword="true"/> when <paramref name="value"/> is a valid address.</returns>
 38    /// <remarks>
 39    /// Surrounding whitespace is not trimmed: the syntax gate stays authoritative, so an address with
 40    /// stray spaces is rejected rather than silently repaired.
 41    /// </remarks>
 42    public static bool TryNormalize(string? value, [NotNullWhen(true)] out string? normalized)
 43    {
 2644        normalized = null;
 45
 2646        if (string.IsNullOrEmpty(value))
 47        {
 248            return false;
 49        }
 50
 2451        var separator = value.LastIndexOf('@');
 52
 2453        if (separator <= 0 || separator == value.Length - 1)
 54        {
 455            return false;
 56        }
 57
 2058        var localPart = value[..separator];
 2059        var domain = value[(separator + 1)..];
 2060        var candidate = $"{localPart}@{NormalizeDomain(domain)}";
 61
 2062        if (!RegexCollection.Email().IsMatch(candidate))
 63        {
 964            return false;
 65        }
 66
 1167        normalized = candidate;
 68
 1169        return true;
 70    }
 71
 72    /// <summary>
 73    /// Checks whether <paramref name="value"/> is a valid email address.
 74    /// </summary>
 75    /// <param name="value">The address to check.</param>
 76    /// <returns><see langword="true"/> when the address is valid.</returns>
 77    /// <remarks>
 78    /// Accepts everything <see cref="RegexCollection.Email"/> accepts, plus mixed-case and
 79    /// internationalized domains, which are normalized before the pattern is applied.
 80    /// </remarks>
 581    public static bool IsValid(string? value) => TryNormalize(value, out _);
 82
 83    private static string NormalizeDomain(string domain)
 84    {
 85        // An address literal such as [192.168.1.1] is not a domain name and must not be punycoded.
 2086        if (domain.StartsWith('['))
 87        {
 288            return domain;
 89        }
 90
 91        try
 92        {
 1893            return Idn.GetAscii(domain).ToLowerInvariant();
 94        }
 395        catch (ArgumentException)
 96        {
 97            // Not a convertible domain; hand it back untouched and let the pattern reject it.
 398            return domain;
 99        }
 18100    }
 101}