Skip to content

CkEmail

Job: turn whatever an email field holds into a clean, lower-case address you can trust — and compare, group, mask or search addresses without writing your own rules.

Email addresses arrive in every shape: "Bob <BOB@Example.com>", "mailto:amy@y.org", an address buried in a sentence, or plain garbage. CkEmail cleans the value, checks that it looks like a real address, and hands you back the bare, lower-cased form. When a value cannot be used, the string methods answer "" — that is the standard CommonKit signal: non-empty in, empty out means rejected. The one exception is Mask, which is made for display and never answers empty.

Quick look

using SafeCoder.CommonKit;

CkEmail.Address("Bob <BOB@Example.com>.");   // -> "bob@example.com"
CkEmail.Address("not-an-email");             // -> ""  (rejected)
CkEmail.Canonical("bob+news@example.com");   // -> "bob@example.com"
CkEmail.Domain("bob@example.com");           // -> "example.com"
CkEmail.Mask("bob@example.com");             // -> "b**@example.com"
CkEmail.IsFreeMail("bob@gmail.com");         // -> true
CkEmail.HasTag("bob+news@example.com");      // -> true
CkEmail.Explain("bob@");          // -> the reader's reason

Methods

Address

The clean, sendable form.

string Address(
    string? input = null,
    int maxLength = CkSettings.MaxEmailInputLength,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
maxLength int CkSettings.MaxEmailInputLength (254) Longest cleaned value accepted, measured after markup removal but before the display name is stripped; the extracted address is held to the same limit. Values below 1 fall back to the default.
fallback string? null Backup value used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a valid address The canonical, lower-cased address: "bob@example.com" The canonical, lower-cased address: "bob@example.com"
Rejected: missing, oversized, or not a valid address Empty string Throws CkEmailException — carries Input, Expected and Reason

Validation is syntactic only — it proves the shape is plausible, not that the mailbox exists.

The workhorse. It strips markup, lower-cases the value, unwraps display names like "Bob <bob@example.com>", peels off sentence punctuation and a "mailto:" prefix, then checks the result parses as one plausible address. This is the form you store and send to. Anything unusable answers "".

CkEmail.Address("Bob <BOB@Example.com>.");       // -> "bob@example.com"
CkEmail.Address("mailto:amy@y.org");             // -> "amy@y.org"
CkEmail.Address("call me at bob@example.com");   // -> "bob@example.com"
CkEmail.Address("not-an-email");                 // -> ""

The check is about shape only — it proves the value looks like an address, not that the mailbox exists. And it is stricter than the raw rules on purpose: quoted local parts ("john doe"@example.com), single-label hosts (user@localhost), addresses with non-ASCII letters, and lists of several addresses are all rejected, because real data that looks like that is almost always wrong.

Canonical

The form for matching, never for sending.

string Canonical(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
fallback string? null Backup address used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a valid address The canonical mailbox form: "bob@example.com" The canonical mailbox form: "bob@example.com"
Rejected: missing, not a valid address, or nothing left once the tag goes Empty string Throws CkEmailException — carries Input, Expected and Reason

For matching, never for sending — the canonical form is not guaranteed to be deliverable as written.

Two spellings can reach the same mailbox. Canonical reduces an address to the mailbox it actually lands in: a "+tag" sub-address is dropped (the part after + that many mail systems let you add for filtering), and for Gmail — where dots in the name are ignored and googlemail.com is the same service — the dots go and the domain becomes gmail.com. Use it to de-duplicate and compare; send mail to what Address returns, because the canonical form is not guaranteed to be deliverable as written.

CkEmail.Canonical("bob+news@example.com");     // -> "bob@example.com"
CkEmail.Canonical("B.o.b+x@googlemail.com");   // -> "bob@gmail.com"
CkEmail.Canonical("+news@gmail.com");          // -> ""  (nothing left once the tag goes)

Same

Do these two reach one mailbox?

bool Same(
    string? a = null,
    string? b = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
a string? null The first value.
b string? null The second value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says which value is not a valid address and why, instead of answering false.

Returns

When Default With throwIfInvalid: true
Both values are valid addresses reaching one mailbox true true
Both are valid addresses, but different mailboxes false false
Rejected: either value is not a valid address false Throws CkEmailException — carries Input, Expected and Reason

A false can mean "different mailboxes" or "not an address" — pass the flag when the difference matters.

The comparison itself: both values are reduced with Canonical and compared. Use it for "is this the account we already have?" checks instead of comparing raw strings.

CkEmail.Same(
    a: "Bob@Example.com",
    b: "bob+news@example.com"
);   // -> true

CkEmail.Same(
    a: "b.o.b@gmail.com",
    b: "bob@googlemail.com"
);   // -> true

CkEmail.Same(
    a: "not-an-email",
    b: "not-an-email"
);   // -> false  (two bad values are two unknowns, not one mailbox)

Domain and Local are the two halves of an address. Both clean and validate the value with Address first, so a bad value answers "".

Domain

The part after the @.

string Domain(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
fallback string? null Backup address used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a valid address The domain, lower-cased: "example.com" The domain, lower-cased: "example.com"
Rejected: missing, oversized, or not a valid address Empty string Throws CkEmailException — carries Input, Expected and Reason

The fallback is a full address — its domain half is returned.

Domain gives the part after the @. It is handy for grouping and reporting.

CkEmail.Domain("Bob <BOB@Example.com>");       // -> "example.com"
CkEmail.Domain("not-an-email");                // -> ""

Local

The part before the @.

string Local(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
fallback string? null Backup address used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a valid address The local part, lower-cased: "bob.smith+news" The local part, lower-cased: "bob.smith+news"
Rejected: missing, oversized, or not a valid address Empty string Throws CkEmailException — carries Input, Expected and Reason

The fallback is a full address — its local half is returned.

Local gives the part before the @ (the "local part"). It works like Domain, but note that Local keeps the "+tag" — use Canonical when you want it gone.

CkEmail.Local("Bob.Smith+news@Example.com");   // -> "bob.smith+news"

Mask

Safe to show, safe to log.

string Mask(
    string? input = null,
    int keepFirst = CkSettings.DefaultMaskEmailKeepFirst,
    char maskChar = '*'
)
Parameter Type Default What it does
input string? null The raw email value to mask.
keepFirst int CkSettings.DefaultMaskEmailKeepFirst (1) How many leading characters of the local part stay visible. Capped at CkSettings.MaxMaskKeepLast (4); negatives count as 0. A local part no longer than this is hidden completely.
maskChar char '*' The replacement character. Whitespace and control characters fall back to '*'.

Returns

When You get
The value is valid The masked address: "b**@example.com"
Missing or invalid The fully masked placeholder: "*****@*****.***" — never empty, and it never throws

The display never leaks and never goes blank.

Hides the local part but keeps the domain, so a log line or a screen can show which provider without showing who. keepFirst (default 1) says how many leading characters stay visible. Made for display, so it never answers empty: a missing or invalid value comes back as the fully masked shape, indistinguishable from a hidden valid one.

CkEmail.Mask("bob@example.com");   // -> "b**@example.com"

CkEmail.Mask(
    input: "bob.smith@example.com",
    keepFirst: 3
);   // -> "bob******@example.com"

CkEmail.Mask("not-an-email");      // -> "*****@*****.***"  (never empty, never the raw value)

InDomains

Is the address on your list?

bool InDomains(
    string? input = null,
    params IEnumerable<string?> domains
)
Parameter Type Default What it does
input string? null The raw email value.
domains IEnumerable<string?> (empty) The domains to match. An entry matches its exact domain; "*.example.com" matches any sub-domain; a leading @ is stripped; null, empty and blank entries are skipped.

Returns

When You get
The address is valid and its domain matches an entry true
No match, an invalid address, or an empty list false — a list test is an answer, and it never throws

A params list must come last, so there is no throwIfInvalid — call Domain with it first when you need the exception.

Checks whether an address belongs to one of the domains you name — an allow-list of partners, a block-list of throwaway providers, routing by company. An entry matches its exact domain; write "*.example.com" to match any sub-domain of example.com. A leading @ on an entry is fine. An invalid address, or an empty list, answers false.

CkEmail.InDomains(
    input: "bob@example.com",
    domains: ["example.com", "partner.org"]
);   // -> true

CkEmail.InDomains(
    input: "bob@mail.example.com",
    domains: ["*.example.com"]
);   // -> true

CkEmail.InDomains(
    input: "bob@example.com",
    domains: ["*.example.com"]
);   // -> false  (the wildcard covers sub-domains only — list "example.com" too)

The list is yours to maintain: any list baked into the library would go stale.

IsRole

A function, not a person.

bool IsRole(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering false, when the value is not a valid address.

Returns

When Default With throwIfInvalid: true
The address is valid and its local part is a known role true true
The address is valid and reads as a person false false
Rejected: not a valid address false Throws CkEmailException — carries Input, Expected and Reason

A false can mean "a person" or "not an address" — pass the flag when the difference matters.

Tells whether the address is a role mailbox — one that reaches a job rather than a person, like info@, support@ or noreply@. Useful for "do not market to this" and "this is not a person" checks. The list is fixed and conservative, so a shared mailbox under an unusual name may still pass as a person.

CkEmail.IsRole("info@example.com");          // -> true
CkEmail.IsRole("noreply+123@example.com");   // -> true  (the "+tag" is ignored)
CkEmail.IsRole("bob@example.com");           // -> false

ExtractAll

Every address in a blob of text.

string[] ExtractAll(
    string? text = null,
    int maxLength = CkSettings.MaxTextInputLength
)
Parameter Type Default What it does
text string? null The text to search; null, empty and whitespace answer an empty array.
maxLength int CkSettings.MaxTextInputLength (50,000) Longest text searched, measured after markup removal; a longer value answers an empty array rather than a partial result. Values below 1 fall back to the default.

Returns

When You get
The text holds addresses Every address found, in Address form: ["bob@x.com", "amy@y.org"]
Nothing to find, or the text is missing or oversized Empty array — never null, and it never throws

An address hidden in an attribute — a mailto: link whose visible text says something else — goes with the markup and is not found.

Searches free text — notes, signatures, pasted mail headers, even HTML — and returns every address found, cleaned the way Address cleans them, in order of first appearance and without duplicates. When there is nothing to find you get an empty array, never null.

CkEmail.ExtractAll("cc Bob <bob@x.com> and amy@y.org.");   // -> ["bob@x.com", "amy@y.org"]
CkEmail.ExtractAll("no addresses here");                   // -> empty array — never null

IsAddress

Is it a valid address at all?

bool IsAddress(
    string? input = null,
    int maxLength = CkSettings.MaxEmailInputLength,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
maxLength int CkSettings.MaxEmailInputLength (254) Maximum length of the cleaned value. Below 1 falls back to the default.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering false, when the value is not a valid address.

Returns

When Default With throwIfInvalid: true
The value parses to one plausible ASCII mailbox, under Address's rules true true
Rejected: not a valid address false Throws CkEmailException — carries Input, Expected and Reason

The yes-or-no form of Address; when the value is going to be stored, call Address instead and check for "", which hands you the canonical spelling.

CkEmail.IsAddress("Bob <BOB@Example.com>.");   // -> true
CkEmail.IsAddress("bob@localhost");            // -> false  (rejected on purpose)

HasTag

Plus-addressed?

bool HasTag(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering false, when the value is not a valid address.

Returns

When Default With throwIfInvalid: true
A valid address whose local part carries a +tag true true
A valid address without one false false
Rejected: not a valid address false Throws CkEmailException — carries Input, Expected and Reason

Sub-addressing is what Canonical strips; this says whether it was there. For "no plus-addressed signups" rules and for spotting one person registering many times. A + with nothing on one side of it is not a tag.

CkEmail.HasTag("bob+news@example.com");   // -> true
CkEmail.HasTag("bob@example.com");        // -> false

SameDomain

At the same domain?

bool SameDomain(
    string? a = null,
    string? b = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
a string? null The first value.
b string? null The second value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says which value is not a valid address and why, instead of answering false.

Returns

When Default With throwIfInvalid: true
Both are valid addresses at the same domain, as Domain hands it out true true
Both are valid, at different domains false false
Rejected: either is not a valid address false Throws CkEmailException — carries Input, Expected and Reason

The "invite your teammates" heuristic, "same organisation". Pair it with IsFreeMail, since two Gmail users share a domain and nothing else. A sub-domain is a different domain here; InDomains with a wildcard is the tool for "the same company". Two values that are not addresses are never at the same domain.

CkEmail.SameDomain("bob@example.com", "AMY@Example.com");      // -> true
CkEmail.SameDomain("bob@example.com", "amy@mail.example.com");  // -> false

IsFreeMail

A consumer mailbox?

bool IsFreeMail(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering false, when the value is not a valid address.

Returns

When Default With throwIfInvalid: true
A valid address at one of the big consumer providers true true
A valid address anywhere else false false
Rejected: not a valid address false Throws CkEmailException — carries Input, Expected and Reason

Gmail, Yahoo, Outlook and Hotmail, iCloud, AOL, Proton, GMX and the like, the country domains of the brands that have them included, yahoo.co.uk, hotmail.de. For "business addresses only" signups. The list is fixed and conservative, like the one behind IsRole: a small or regional provider passes as business, and the brand as someone else's sub-domain, yahoo.example.com, is not the provider — nor is the brand under any other ending, hotmail.evil: after the brand comes a country code or one of the endings the providers use, .com, .net, .org.

CkEmail.IsFreeMail("bob@gmail.com");       // -> true
CkEmail.IsFreeMail("bob@yahoo.co.uk");     // -> true
CkEmail.IsFreeMail("bob@safecoder.com");   // -> false

HasDisplayName

Written with a name in front?

bool HasDisplayName(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw email value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkEmailException that says why, instead of answering false, when the value is not a valid address.

Returns

When Default With throwIfInvalid: true
A valid address written with a name before it, "Bob <bob@example.com>" or "Bob bob@example.com", read as Address reads it true true
A bare address, or brackets with nothing before them false false
Rejected: not a valid address false Throws CkEmailException — carries Input, Expected and Reason

For intake contracts that demand the bare form; Address strips the name either way, so the address inside still reads.

CkEmail.HasDisplayName("Bob Smith <bob@example.com>");   // -> true
CkEmail.HasDisplayName("bob@example.com");                // -> false

Explain

Why not — in words, without an exception.

string Explain(
    string? input = null,
    int maxLength = CkSettings.MaxEmailInputLength
)
Parameter Type Default What it does
input string? null The raw email value.
maxLength int CkSettings.MaxEmailInputLength (254) Maximum length of the cleaned value.

Returns

When You get
The value passes the reader ""
It does not The reason, the same words the exception would carry — an answer, not a failure; it never throws

The form-friendly twin of throwIfInvalid: the message to show beside the field, without a try and catch. Every class with a reader has one, on the reader's own rules, so the reason is exactly what the reader would say.

CkEmail.Explain("bob@example.com");   // -> ""
CkEmail.Explain("");                  // -> "no value"

The fallback

Address, Canonical, Domain and Local take a fallback — a spare address used when the input is rejected. The fallback goes through the exact same cleaning and validation and is never returned as typed; a bad fallback answers "" too. For Domain and Local the fallback is a full address, and its half is returned. The validators take no fallback — their answer is false.

CkEmail.Address(
    input: "",
    fallback: "office@example.com"
);   // -> "office@example.com"  (the field was empty, so the spare is used)

Good to know

  • Non-empty in, empty out means rejected. Check for "" where it matters — that is the signal across every CommonKit string method.
  • A look-alike domain is refused in its encoded form too. Non-ASCII addresses are rejected; their encoded xn-- spelling is ASCII and accepted — bob@xn--bcher-kva.example is bücher — unless the label decodes to letters of more than one alphabet, the way xn--pypal-4ve decodes to pаypal with a Cyrillic а, which a mail client would show pixel-identical to the real name. The reason names it: "a look-alike domain label: its decoded form mixes alphabets".
  • There is deliberately no EmailOrNull. When your database column wants NULL instead of an empty string, bridge with CkText:
    CkText.NullIfEmpty(CkEmail.Address("not-an-email"));   // -> null
    
  • Validation is shape-only. A value that passes still might not be a real, working mailbox — only sending to it proves that.
  • Some legal addresses are rejected on purpose. Quoted local parts, user@localhost, bracketed IP domains, non-ASCII (internationalized) addresses and lists of addresses are all legal on paper and all answer "" here, because in ordinary data they are almost never legitimate.
  • Canonical is for matching, never for sending. Store and send what Address returns; use Canonical (or Same) to decide whether two values are one mailbox.
  • Same never matches two bad values. Two rejected inputs answer false, not "equal" — two unknowns are not one mailbox.
  • The wildcard in InDomains skips the apex. "*.example.com" matches sub-domains only; add "example.com" as its own entry to cover the bare domain.
  • Mask never answers empty. A missing or invalid value shows as "*****@*****.***" — a display always has something safe to show.
  • SameDomain and IsFreeMail go together. A shared domain means colleagues only when it is not a consumer provider; check both before treating two people as one organisation.
  • IsFreeMail's list is fixed, InDomains's is yours. The consumer providers change slowly enough to ship; disposable-address domains do not, so that list stays in your application and goes through InDomains.

If you want errors instead

Pass throwIfInvalid: true and a value that cannot be used raises a CkEmailException (from SafeCoder.CommonKit.Exceptions) instead of answering "" or false. The exception carries the Input and a short Reason, flattened into a one-line message. Address, Canonical, Same, Domain, Local, IsRole and the new validators — IsAddress, HasTag, SameDomain, IsFreeMail, HasDisplayName — take it; Mask, ExtractAll and InDomains do not — a display always has something to show, and a search that finds nothing or a list test that says no are answers, not failures. When you need the exception before a list test, call Domain with throwIfInvalid: true first.

CkEmail.Address(
    input: "not-an-email",
    throwIfInvalid: true
);   // throws CkEmailException — the message says why