emailverifier-kt is a composable, pluggable Kotlin library for validating email addresses beyond just their syntax. It's built with a clear focus: help developers reliably assess whether a given email is real, meaningful, and worth accepting.
emailverifier-kt is designed for high performance and uses Kotlin's coroutines to parallelize I/O operations:
- Parallel Initialization: All external data sources (e.g., Public Suffix List, disposable domains) are downloaded concurrently during setup, making initialization significantly faster.
- Parallel Verification: Independent network checks (MX records, Gravatar) are executed concurrently for each email, reducing the verification time.
emailverifier-kt performs a layered set of validations:
Checks the structure of the email:
- Local-part format (dot-atom and quoted-string, per RFC 5322 subset)
- Hostname validity (RFC 1035, IDNA-compliant)
Verifies whether the email domain is registrable:
- Uses the Public Suffix List
- Returns the registrable domain (e.g.,
example.co.ukforuser@example.co.uk) orFailedif not registrable (e.g.,user@something.invalid).
Ensures the domain is actually configured to receive emails:
- Queries DNS-over-HTTPS (DoH) via Google
- Returns a list of MX records or
Failedif no MX records are found.
Filters out temporary/disposable email domains:
- Uses curated lists from disposable-email-domains
- Detects domains like
mailinator.com,tempmail.org, etc. - You can also specify
allowanddenysets to customize the behavior for specific domains.
Detects whether an email has an associated Gravatar:
- Computes MD5 hash of the email
- Returns the Gravatar URL or
Failedif no custom avatar is found.
Checks whether the email domain belongs to a known free‐email provider (e.g. gmail.com, yahoo.com)
using a curated list of popular services.
- Returns
Passedresult if email hostname is not a known free‐email provider - You can also specify
allowanddenysets to customize the behavior for specific domains.
List used: Github gist by @okutbay
Detects generic or departmental username (e.g. info@, admin@, support@) by checking against a curated list of common role-based usernames.
- Returns
Passedresult if email username is not a known role-based username - You can also specify
allowanddenysets to customize the behavior for specific usernames.
List used: https://github.com/mbalatsko/role-based-email-addresses-list (original repo: https://github.com/mixmaxhq/role-based-email-addresses)
Performs a live check with the mail server to verify if the mailbox actually exists.
- Connects to the mail server and uses the
RCPT TOcommand to check for deliverability without sending an email. - Can detect "catch-all" server configurations where all emails to a domain are accepted.
- Disabled by default, as most cloud providers and ISPs block outbound traffic on port 25 to prevent spam. Can be enabled and configured to work through a SOCKS proxy.
For environments without internet access, EmailVerifier can run in a fully offline mode. When enabled, it uses bundled
data for checks that support it (Syntax, Registrability, Disposability, Free Email, and Role-Based Username) and automatically
disables checks that require network access (MX Record, Gravatar, SMTP).
You can also configure offline mode for each check individually.
The bundled data is manually updated before release via a GitHub Actions workflow.
You get a detailed result for each check:
data class EmailValidationResult(
val email: String,
val emailParts: EmailParts,
val syntax: CheckResult<SyntaxValidationData>,
val registrability: CheckResult<RegistrabilityData>,
val mx: CheckResult<MxRecordData>,
val disposable: CheckResult<DatasetData>,
val gravatar: CheckResult<GravatarData>,
val free: CheckResult<DatasetData>,
val roleBasedUsername: CheckResult<DatasetData>,
val smtp: CheckResult<SmtpData>,
) {
/**
* Returns true if all strong indicator checks passed.
* Strong indicator checks are: syntax, registrability, mx record presence, and disposability.
* These checks are the most likely to indicate that an email address is not valid.
*/
fun isLikelyDeliverable(): Boolean
}
/**
* A sealed class representing the result of a single validation check.
* It can be in one of four states: Passed, Failed, Skipped, or Errored.
*
* @param T the type of data carried by the result.
*/
sealed class CheckResult<out T> {
/**
* Indicates that the check was successful.
* @property data data associated with the passed check.
*/
data class Passed<T>(
val data: T,
) : CheckResult<T>()
/**
* Indicates that the check failed.
* @property data optional data associated with the failed check.
*/
data class Failed<T>(
val data: T? = null,
) : CheckResult<T>()
/**
* Indicates that the check was skipped.
*/
data object Skipped : CheckResult<Nothing>()
/**
* Indicates that the check produced an error.
* @property error the throwable that was caught during the check.
*/
data class Errored(
val error: Throwable,
) : CheckResult<Nothing>()
}
/**
* Data class holding the validity of each part of the email syntax.
* @property username true if the username part is valid.
* @property plusTag true if the plus-tag part is valid.
* @property hostname true if the hostname part is valid.
*/
data class SyntaxValidationData(
val username: Boolean,
val plusTag: Boolean,
val hostname: Boolean,
)
/**
* Data class holding the registrable domain found during the registrability check.
* @property registrableDomain The registrable domain string, or null if not found.
*/
data class RegistrabilityData(
val registrableDomain: String?,
)
/**
* Data class holding the MX records found during the MX record check.
* @property records A list of [MxRecord]s, or an empty list if none were found.
*/
data class MxRecordData(
val records: List<MxRecord>,
)
/**
* Data class holding the Gravatar URL found during the Gravatar check.
* @property gravatarUrl The Gravatar URL string, or null if no custom avatar was found.
*/
data class GravatarData(
val gravatarUrl: String?,
)
/**
* Data class holding the result of a dataset check (disposable, free, role-based).
*
* @property match true if a match was found in the dataset.
* @property matchedOn the specific entry that was matched, or null if no match was found.
* @property source the source of the match (e.g., "allow", "deny", "default").
*/
data class DatasetData(
val match: Boolean,
val matchedOn: String? = null,
val source: Source? = null,
)
/**
* Data class holding the results of an SMTP check.
*
* @property isDeliverable true if the email address is deliverable.
* @property isCatchAll true if the server has a catch-all policy, false if not, null if inconclusive.
* @property smtpCode the last SMTP response code.
* @property smtpMessage the last SMTP response message.
*/
data class SmtpData(
val isDeliverable: Boolean,
val isCatchAll: Boolean?,
val smtpCode: Int,
val smtpMessage: String,
)Each check can return:
Passed✅ (with optional data, see data classes above for details)Failed❌ (with optional data, see data classes above for details)Errored⚠️ (if an unexpected error occurred during the check)Skipped⏭️ (if not enabled or not applicable)
For Disposable Email Detection, Free Email Provider Detection, and Role-Based Username Detection, the result is a CheckResult<DatasetData>. The Passed state indicates the email is not disposable/free/role-based, while Failed indicates it is. The DatasetData object provides more context, including the specific rule or entry that was matched.
Maven:
<dependency>
<groupId>io.github.mbalatsko</groupId>
<artifactId>emailverifier-kt</artifactId>
<version>LATEST_VERSION</version>
</dependency>Gradle:
implementation("io.github.mbalatsko:emailverifier-kt:LATEST_VERSION")Also available on Github Packages
val verifier = emailVerifier { }
val result = verifier.verify("john.doe@example.com")
if (result.isLikelyDeliverable()) {
println("Valid email!")
} else {
println("Email validation failed: $result")
}All checks are enabled by default, but you can easily disable or configure them.
val verifier = emailVerifier {
// Disable a check
mxRecord {
enabled = false
}
registrability {
customRules = setOf(
"my-private-tld", // Treat .my-private-tld as a public suffix
"*.my-private-domain", // Treat all subdomains of .my-private-domain as public suffixes
"!example.my-private-domain" // Make an exception to the wildcard rule
)
}
// Configure allow/deny lists for dataset checks
disposability {
allow = setOf("my-disposable-domain.com") // Whitelist a disposable domain
deny = setOf("my-domain.com") // Blacklist a domain
}
// Configure SMTP parameters
smtp {
enabled = true // IMPORTANT: Disabled by default. See notes below.
timeoutMillis = 10000 // Increase connection timeout
}
}
⚠️ Important Note on SMTP Checks The SMTP check is disabled by default because most Internet Service Providers (ISPs) and cloud hosting providers (like AWS, GCP, Azure) block outgoing requests on port 25 to prevent email spamming.To perform this check reliably, you will likely need to route the connection through a SOCKS proxy that has unrestricted access to port 25.
Here is how you can configure it:
import java.net.InetSocketAddress import java.net.Proxy val verifier = emailVerifier { smtp { enabled = true // Configure a SOCKS proxy proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("your-proxy-host.com", 1080)) } }
For checks that rely on external datasets (Registrability, Disposability, Free Email, and Role-Based Username), you have full control over the data source.
For ultimate convenience, you can set the global allOffline flag. This forces all checks to use their bundled offline data and disables checks that require a network connection (MX, Gravatar, SMTP). This is the simplest way to configure the verifier for an environment with no internet access.
val verifier = emailVerifier {
allOffline = true
}
val result = verifier.verify("mbalatsko@gmail.com")
// result.mx will be SKIPPED
// result.gravatar will be SKIPPED
// result.smtp will be SKIPPEDYou can also configure the data source for each check individually.
The offline property provides a simple toggle between the default remote URL and the default bundled data source for a specific check.
val verifier = emailVerifier {
// Use the bundled offline data for this check
registrability {
offline = true
}
// Use the online source for this one (default behavior)
disposability {
offline = false
}
}For complete control, you can provide a custom data source using the source property. This is ideal for using proprietary lists, testing, or managing datasets locally. The DataSource type ensures your configuration is clear and type-safe.
import io.github.mbalatsko.emailverifier.DataSource
val verifier = emailVerifier {
// Use a custom remote URL
registrability {
source = DataSource.Remote("https://my.custom.domain/public_suffix_list.dat")
}
// Use a custom file from your classpath resources
disposability {
source = DataSource.Resource("/my_disposable_domains.txt")
}
// Use a custom file from the local filesystem
free {
source = DataSource.File("/path/to/your/free_emails.txt")
}
}The default HttpClient used by EmailVerifier is configured with a sensible retry policy (retryOnServerErrors(maxRetries = 3) with exponential backoff) to handle transient network issues.
For more advanced use cases, such as adding custom headers or using a different engine, you can pass a custom-configured HttpClient to the EmailVerifier. This gives you full control over the network layer.
Here's an example of how to configure a custom client:
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.logging.*
// Configure a custom HttpClient
val customHttpClient = HttpClient(CIO) {
install(Logging) {
level = LogLevel.INFO
}
// The default retry logic is not included when providing a custom client.
// You can add it back if needed:
// install(HttpRequestRetry) {
// retryOnServerErrors(maxRetries = 3)
// exponentialDelay()
// }
}
// Pass the custom client in the configuration
val verifier = emailVerifier {
httpClient = customHttpClient
}The emailVerifier {} call performs several network requests to download the necessary data for the various checks.
To avoid re-downloading this data every time you want to verify an email, it is highly recommended to create a single
instance of the EmailVerifier and reuse it throughout the lifecycle of your application.
For long-running applications, it's often necessary to refresh the data used by the verifier without restarting the application.
EmailVerifier provides a set of suspend functions to reload the data for the checks that use external datasets.
These functions are thread-safe and will fetch the latest data from the configured DataSource (remote, file, or resource).
val verifier = emailVerifier {
// Your configuration...
}
// Refresh the Public Suffix List data
verifier.updateRegistrabilityCheckerData()
// Refresh the disposable email domains data
verifier.updateDisposableCheckerData()
// Refresh all data sources in parallel
verifier.updateAllData()This is particularly useful if you want to keep your disposable email lists or other datasets up-to-date by periodically calling these methods.
emailverifier-kt uses the SLF4J logging facade. This allows you, as a user of the library, to choose your own logging framework (e.g., Logback, Log4j 2, slf4j-simple). The library itself only includes the slf4j-api dependency, so it does not force a specific logging implementation on your application.
By default, no logs will be produced unless you add a logging implementation to your project's dependencies.
To see the logs from EmailVerifier, you need to add a dependency on an SLF4J implementation. For example, to use a simple logger that prints to standard output, you can add the following Gradle dependency:
testImplementation("org.slf4j:slf4j-simple:2.0.13")You can configure the log levels for the library's loggers to control the amount of output. The main logger categories are:
io.github.mbalatsko.emailverifier.EmailVerifierDslBuilder: Logs the configuration and building process of theEmailVerifier.io.github.mbalatsko.emailverifier.EmailVerifier: Logs the overall verification process for each email.io.github.mbalatsko.emailverifier.components.checkers.*: Loggers for individual checks (e.g.,GravatarChecker,SmtpChecker).io.github.mbalatsko.emailverifier.components.core.*: Loggers for core components likeGoogleDoHLookupBackendandSocketSmtpConnection.io.github.mbalatsko.emailverifier.components.providers.*: Loggers for data providers likeOnlineLFDomainsProvider.
For example, with Logback, you could set the log level for the entire library to DEBUG by adding the following to your logback.xml:
<logger name="io.github.mbalatsko.emailverifier" level="DEBUG"/>Planned features:
- Typo check suggestions
- Multiplatform Support
- Support Kotlin/Native by replacing or abstracting away java.net.IDN
- ✅ JVM
- ❌ Native/JS (pending IDN/punycode compatibility layer)
Issues, suggestions, and PRs welcome. Aim is correctness, composability, and pragmatic coverage — not full RFC simulation.