I was implementing a signup endpoint for an API where users can register via email. There are many ways to verify an email address. You could use a popular library like go-playground/validator to check if the input is in an email format. However, this doesn't confirm if the email address actually exists or if its domain can receive mail.
If you prefer not to use regex or an external package, Go's standard library offers the net/mail package to validate an email address format.
This approach is generally better than a simple regex validation because mail.ParseAddress parses an address according to the official RFC 5322 specification, which defines the standard for email format.
Basic Format Validation
Let's start by checking if a string has a valid email format.
Example 1: A Well-Formatted but Non-Existent Email
email := "example-user@exampledomain.com"
_, err := mail.ParseAddress(email)
if err != nil {
log.Panic(err)
}
This code will run without an error. It passes the test because the string contains a local part (example-user), an @ symbol, and a domain (exampledomain.com). The ParseAddress function only cares about the format, not whether the address is real.
Example 2: An Incorrect Format
email := "example-user"
_, err := mail.ParseAddress(email)
if err != nil {
log.Panic(err)
}
In this case, the program will panic and show the error: mail: missing '@' in address.
Validating the Domain
Format validation is a good first step, but it doesn't prevent users from signing up with an address like test@nonexistent-domain.com. Our next thought might be to check the domain's DNS records.
A common but flawed approach is to look up the host directly.
import "net"
domain := "exampledomain.com"
_, err := net.LookupHost(domain)
if err != nil {
log.Panic(err)
}
This check for an A or AAAA record is not what we need for email validation. Many people purchase domains exclusively for email services and never set up a website, so they may not have an A record pointing to a web server. Letting users pass verification based on this check is a bad idea.
The correct approach is to check for MX (Mail Exchanger) records. These DNS records specify the mail servers responsible for accepting email on behalf of a domain. If a domain has MX records, it's configured to receive email.
Looking Up MX Records
First, let's get the domain name from the email address and use net.LookupMX to find the MX records for that domain.
email := "contact@gmail.com"
parts := strings.Split(email, "@")
domain := parts[1] // "gmail.com"
mxRecords, err := net.LookupMX(domain)
if err != nil {
log.Panicf("Failed to look up MX records for %s: %v", domain, err)
}
if len(mxRecords) == 0 {
log.Fatalf("No MX records found for %s", domain)
}
fmt.Printf("MX Records for %s:\n", domain)
for _, mx := range mxRecords {
fmt.Printf(" Host: %s, Preference: %d\n", mx.Host, mx.Pref)
}
For a valid address like contact@gmail.com, you'll see a result similar to this, confirming the domain is set up to receive mail:
MX Records for gmail.com:
Host: gmail-smtp-in.l.google.com., Preference: 5
Host: alt1.gmail-smtp-in.l.google.com., Preference: 10
Host: alt2.gmail-smtp-in.l.google.com., Preference: 20
Host: alt3.gmail-smtp-in.l.google.com., Preference: 30
Host: alt4.gmail-smtp-in.l.google.com., Preference: 40
If you run the same code on a domain with no MX records, net.LookupMX will return an error, such as lookup joybtw.com: no such host. This tells us the domain is not configured to receive email, and we should reject the address.
The SMTP Handshake to Verify the Mailbox
Now that we know the domain is configured to receive mail, we can go one step further by asking the server if the specific mailbox (e.g., contact@gmail.com) actually exists. We can do this by performing a partial SMTP handshake.
This process involves connecting to the mail server and simulating the first few steps of sending an email, stopping just before transmitting any actual content. The key is the RCPT TO command, where we ask the server if it will accept a message for our target email address. The server's response tells us whether the user likely exists.
Here’s how it works:
- Connect to the Mail Server: We connect to the mail server with the highest priority (lowest preference number) on port 25.
- Start the Conversation (
EHLO): We greet the server to start the conversation. - Specify the Sender (
MAIL FROM): We state the sender's address. - Check the Recipient (
RCPT TO): We provide the email address to validate.- A response code starting with
250means "OK," and the address is likely valid. - A response code like
550means the address does not exist.
- A response code starting with
- End the Conversation (
QUIT): We properly close the connection without sending an email.
Important Considerations
While powerful, this method isn't foolproof:
- Catch-All Addresses: Some servers are configured with a "catch-all" policy, meaning they accept emails for any address at that domain to prevent spammers from guessing valid addresses. In this case, the
RCPT TOcheck will always succeed, even for non-existent mailboxes. - Security Measures: Mail servers may use security features like greylisting or block automated checks altogether. This can lead to false negatives, where a valid address appears invalid.
Here is the complete code:
Here is a simple, runnable Go program that combines these validation steps.
package main
import (
"bufio"
"fmt"
"log"
"net"
"net/mail"
"strings"
)
func main() {
email := "contact@gmail.com" // Change this to test other emails
// Validate Email Format
_, err := mail.ParseAddress(email)
if err != nil {
log.Fatalf("'%s' is not a valid email format: %v", email, err)
}
// Extract Domain and Find MX Records
i := strings.LastIndexByte(email, '@')
domain := email[i+1:]
mxRecords, err := net.LookupMX(domain)
if err != nil {
log.Fatalf("Could not look up MX records for '%s': %v", domain, err)
}
if len(mxRecords) == 0 {
log.Fatalf("No MX records found for '%s'.", domain)
}
// Perform SMTP Handshake
mxServer := mxRecords[0].Host
conn, err := net.Dial("tcp", mxServer+":25")
if err != nil {
log.Fatalf("Could not connect to SMTP server: %v", err)
}
defer conn.Close()
reader := bufio.NewReader(conn)
resp, err := reader.ReadString('\n')
if err != nil {
log.Fatalf("Could not read from connection: %v", err)
}
fmt.Printf("S: %s", resp)
// EHLO command
fmt.Fprintf(conn, "EHLO mydomain.com\r\n")
resp, _ = reader.ReadString('\n')
fmt.Printf("S: %s", resp)
// MAIL FROM command
fmt.Fprintf(conn, "MAIL FROM:<test@mydomain.com>\r\n")
resp, _ = reader.ReadString('\n')
fmt.Printf("S: %s", resp)
// RCPT TO command (The actual check)
fmt.Fprintf(conn, "RCPT TO:<"+email+">\r\n")
resp, _ = reader.ReadString('\n')
fmt.Printf("S: %s", resp)
// Check the response for the recipient
if strings.HasPrefix(resp, "250") {
fmt.Println("Result: Email address is likely valid.")
} else {
fmt.Println("Result: Email address is likely invalid.")
}
// QUIT
fmt.Fprintf(conn, "QUIT\r\n")
resp, _ = reader.ReadString('\n')
fmt.Printf("S: %s", resp)
}
Note: This code is for educational purposes to demonstrate the concepts of email validation. It is not production-level code. For a real-world application, you would need more robust error handling, connection timeouts, and logic to iterate through multiple MX records if the primary one fails.
Top comments (0)