Email Verification in Go
This tutorial adds email verification to a Go HTTP server using standard library primitives and a lightweight Mailbeam REST client.
Prerequisites
- Go 1.21+
- A Mailbeam API key (sign up free)
The Mailbeam client
// internal/mailbeam/client.go
package mailbeam
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
const baseURL = "https://api.mailbeam.dev"
type Client struct {
apiKey string
httpClient *http.Client
}
type VerifyResult struct {
Valid bool `json:"valid"`
Score int `json:"score"`
Disposable bool `json:"disposable"`
CatchAll bool `json:"catchAll"`
Reason *string `json:"reason"`
LatencyMs int `json:"latency_ms"`
}
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
httpClient: &http.Client{Timeout: 5 * time.Second},
}
}
func (c *Client) Verify(ctx context.Context, email string) (*VerifyResult, error) {
body, _ := json.Marshal(map[string]string{"email": email})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/v1/verify", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("mailbeam: unexpected status %d", resp.StatusCode)
}
var result VerifyResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}Verification middleware
// internal/middleware/verify_email.go
package middleware
import (
"context"
"encoding/json"
"log"
"net/http"
"strings"
"sync"
"myapp/internal/mailbeam"
)
type cachedResult struct {
result *mailbeam.VerifyResult
}
var (
cache sync.Map
mbClient *mailbeam.Client
)
func Init(apiKey string) {
mbClient = mailbeam.NewClient(apiKey)
}
// VerifyEmailMiddleware reads email from JSON body and verifies it.
// Fails open on API errors.
func VerifyEmailMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest)
return
}
email := strings.ToLower(strings.TrimSpace(body.Email))
if email == "" {
http.Error(w, `{"error":"email is required"}`, http.StatusBadRequest)
return
}
// Check cache
if cached, ok := cache.Load(email); ok {
result := cached.(cachedResult).result
if !result.Valid || result.Score < 60 {
writeError(w, result.Reason)
return
}
// Pass body back via context
next.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), emailKey{}, email),
))
return
}
// Call Mailbeam
result, err := mbClient.Verify(r.Context(), email)
if err != nil {
log.Printf("[mailbeam] verify error: %v", err)
// Fail open — continue without blocking
next.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), emailKey{}, email),
))
return
}
cache.Store(email, cachedResult{result: result})
if !result.Valid || result.Score < 60 {
writeError(w, result.Reason)
return
}
next.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), emailKey{}, email),
))
})
}
type emailKey struct{}
func EmailFromContext(ctx context.Context) string {
v, _ := ctx.Value(emailKey{}).(string)
return v
}
func writeError(w http.ResponseWriter, reason *string) {
msg := "Please provide a valid email address."
if reason != nil {
switch *reason {
case "disposable_domain":
msg = "Please use a permanent email, not a temporary one."
case "no_mx_records":
msg = "This email domain cannot receive mail."
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}Register on your signup handler
// main.go
package main
import (
"encoding/json"
"net/http"
"os"
"myapp/internal/middleware"
)
func main() {
middleware.Init(os.Getenv("MAILBEAM_KEY"))
mux := http.NewServeMux()
mux.Handle("POST /api/auth/signup",
middleware.VerifyEmailMiddleware(http.HandlerFunc(signupHandler)),
)
http.ListenAndServe(":8080", mux)
}
func signupHandler(w http.ResponseWriter, r *http.Request) {
email := middleware.EmailFromContext(r.Context())
// create user with verified email...
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"email": email})
}Best practices
sync.Mapis safe for concurrent use but unbounded — add TTL with a periodic cleanup or use a proper cache likeristretto- Keep timeout at 5 seconds to avoid long-running signups
- Always fail open: log errors but call
next.ServeHTTP