Mailbeam
Go + net/httpIntermediate20 minutesUpdated January 2025

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


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.Map is safe for concurrent use but unbounded — add TTL with a periodic cleanup or use a proper cache like ristretto
  • Keep timeout at 5 seconds to avoid long-running signups
  • Always fail open: log errors but call next.ServeHTTP

Next steps