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.exampleis bücher — unless the label decodes to letters of more than one alphabet, the wayxn--pypal-4vedecodes 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. Canonicalis for matching, never for sending. Store and send whatAddressreturns; useCanonical(orSame) to decide whether two values are one mailbox.Samenever matches two bad values. Two rejected inputs answerfalse, not "equal" — two unknowns are not one mailbox.- The wildcard in
InDomainsskips the apex."*.example.com"matches sub-domains only; add"example.com"as its own entry to cover the bare domain. Masknever answers empty. A missing or invalid value shows as"*****@*****.***"— a display always has something safe to show.SameDomainandIsFreeMailgo 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 throughInDomains.
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