When sending emails to customers, it’s crucial they actually reach their inboxes. But not all email addresses are valid, leading to bounce-backs that can harm our sender reputation and even get our AWS server flagged.
We’ve faced issues where emails to test and production addresses sometimes fail, causing bounce rates and occasional AWS warnings. To solve this, we’re creating our own validator in Golang. It will check each email address before sending to ensure it’s correctly formatted, importantly email is responding, preventing wasted resources and safeguarding our AWS status.
Why build our own email validator?
As there are thousands of companies providing this service, why build our own? As being early in your startup or growing business, you have to provide them a sum of money for a certain number of each email that they validate and for a growing business it is not feasible to provide that amount of money to those services. In the world of startups and growing businesses, every penny counts. While there are many companies offering email validation services, relying on them means shelling out money for every batch of emails we verify. For a budding enterprise, that expense can quickly add up and strain our budget.
By building our own validator, powered by Golang, we’re taking control of our costs and efficiency. No more paying per email — we’ll handle validation in-house, ensuring accuracy without breaking the bank. It’s not just about saving money; it’s about empowering our growth and ensuring our resources are used where they matter most — building our business.
Let’s build
First let’s initialize our project
go mod init [your repo or folder]
Process involved in building email validator:
0. Validate whether email is correctly formatted
// Validate the syntax of the email address
func isValidEmail(email string) bool {
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return re.MatchString(email)
}
1. Verify a single email address
Now, first we will build up the logic to verify single emails and later move on with multiple email validation. Also declare the return type of the validator.
type VerificationResult struct {
Success bool `json:"success"`
Email string `json:"email"`
}
// Verify a single email address
func verifySingleEmail(email string, timeout time.Duration) VerificationResult {
isValid := isValidEmail(email)
hasMx := false
isDeliverable := false
if isValid {
mxRecords, err := getMXRecords(strings.Split(email, "@")[1])
hasMx = err == nil && len(mxRecords) > 0
if hasMx {
isDeliverable = isDeliverableEmail(email, timeout)
}
}
return VerificationResult{
Success: isValid && hasMx && isDeliverable,
Email: email,
}
}
Here, during verification first we will make sure if the email is correctly formatted. In second steps we will initialize assuming it does not have any MX and is not deliverable. Now if email is valid we will go to the next step of getting MX records of that host like gmail.com, or any particular domain.
// Check if the domain has valid MX records and return them
func getMXRecords(domain string) ([]*net.MX, error) {
mxRecords, err := net.LookupMX(domain)
return mxRecords, err
}
With the help of the built-in net package we will lookup the MX and return. If it has MX with length greater than zero and there is no error we will move into next steps of Making Sure if Email is deliverable or not.
2. Check if email is deliverable
Now let’s write the isDeliverableEmail function to check if email is deliverable.
// Check if the email address is deliverable
func isDeliverableEmail(email string, timeout time.Duration) bool {
parts := strings.Split(email, "@")
domain := parts[1]
mxRecords, err := getMXRecords(domain)
if err != nil || len(mxRecords) == 0 {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var client *smtp.Client
for _, mx := range mxRecords {
client, err = dialWithTimeout(ctx, fmt.Sprintf("%s:%d", mx.Host, 25))
if err == nil {
break
}
}
if err != nil {
return false
}
defer client.Close()
client.Hello("gmail.com")
client.StartTLS(&tls.Config{InsecureSkipVerify: true})
client.Mail("ayush@gmail.com")
err = client.Rcpt(email)
return err == nil
}
Here first, let’s split our email into two parts that is user and domain. Now, we will get all the MX records for that domain such that we can make sure we can ping to that email.
After creating a context with timeout we will be able to pass timeout in our upcoming function if any request takes more time we can cancel it.
In next steps we have created the client and looped through all the MX records in order to validate if that host is valid or not.
func dialWithTimeout(ctx context.Context, addr string) (*smtp.Client, error) {
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return smtp.NewClient(conn, host)
}
This function will try establish connection with the host and we will create a smtp Client with the host and connection.
Now, if it is deliverable we will set isDeliverable to true and if MX, isValid, and isDeliverable is true then email is deliverable otherwise it is not.
3. Validation Multiple email at once by simple and Batch validation
Lets first create the worker function that will be running concurrently and verify emails.
func worker(emails <-chan string, results chan<- VerificationResult, wg *sync.WaitGroup, timeout time.Duration) {
defer wg.Done()
for email := range emails {
results <- verifySingleEmail(email, timeout)
}
}
This function will have channels that will be sending email and receiving results. Now, let’s write the wrapper function for it.
func verifyEmails(emails []string, timeout time.Duration, numWorkers int) map[string]VerificationResult {
emailChan := make(chan string, len(emails))
resultChan := make(chan VerificationResult, len(emails))
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(emailChan, resultChan, &wg, timeout)
}
for _, email := range emails {
emailChan <- email
}
close(emailChan)
wg.Wait()
close(resultChan)
results := make(map[string]VerificationResult)
for result := range resultChan {
results[result.Email] = result
}
return results
}
We will create two channels, one is email and another is result channels to pass values between go routines. Now we will create the WaitGroup such that it only ends the entire program after all the goroutines are destroyed.
func main() {
emails := []string{
"ayush@test.io",
"rupak@test.io",
"rupak124@test.io",
// Add more emails as needed
}
timeout := 10 * time.Second
numWorkers := 10 // Adjust the number of workers based on your system's capability
// Batch verification
results := verifyEmails(emails, timeout, numWorkers)
for _, result := range results {
resultJson, _ := json.Marshal(result)
fmt.Println(string(resultJson))
}
}
Batch Processing
Here if you have some emails or emails that are in csv you can extract all the emails and store in an array. Now, after all the emails are in an array you can simply create the batch processor with following steps.
func main() {
batchSize := 100
for i := 0; i < len(emails); i += batchSize {
end := i + batchSize
if end > len(emails) {
end = len(emails)
}
batch := emails[i:end]
results := verifyEmails(batch, timeout, numWorkers)
for _, result := range results {
// write to csv in the format of email, success
resultJson, _ := json.Marshal(result)
fmt.Println(string(resultJson))
}
}
}
Conclusion
Building an email validator is as easy as this. Now, you can convert this into your need and make an API for your server or also run this function in a serverless environment.
Happy coding!