Rez Moss

Rez Moss

Personal Musings: A Blog for the Tech-Savvy and Curious Mind

Understanding Go's net/mail.Address and Parser 1/3

Jan 2025

Hey there! Today we’re going to dive into Go’s net/mail package, focusing on how it handles email addresses and parsing. If you’ve ever needed to work with email addresses in Go, you’ve probably encountered this package. Let’s break it down and see how to use it effectively.

Getting Started with mail.Address

The mail.Address type is the foundation of email address handling in Go. It’s a simple struct with two fields:

package main

import (
    "fmt"
    "net/mail"
)

func main() {
    // Creating a mail.Address directly
    addr := &mail.Address{
        Name:    "John Smith",
        Address: "[email protected]",
    }
    
    fmt.Printf("Name: %s\nEmail: %s\n", addr.Name, addr.Address)
    
    // String representation
    fmt.Printf("Full address: %s\n", addr.String())
}

Running this will give you a properly formatted email address string. The String() method handles all the necessary quoting and escaping for you.

Parsing Email Addresses

Let’s look at how to parse email addresses. There are a few ways to do this:

func parseExamples() error {
    // Simple case
    addr, err := mail.ParseAddress("[email protected]")
    if err != nil {
        return fmt.Errorf("parsing simple address: %w", err)
    }
    
    // With display name
    addr2, err := mail.ParseAddress("John Smith <[email protected]>")
    if err != nil {
        return fmt.Errorf("parsing address with name: %w", err)
    }
    
    // With quoted name containing special characters
    addr3, err := mail.ParseAddress(`"Smith, John" <[email protected]>`)
    if err != nil {
        return fmt.Errorf("parsing quoted name: %w", err)
    }
    
    fmt.Printf("Simple: %v\n", addr)
    fmt.Printf("With name: %v\n", addr2)
    fmt.Printf("Quoted name: %v\n", addr3)
    
    return nil
}

The parser is pretty forgiving and handles various formats, but it’s good to know what it expects: - Bare email addresses: [email protected] - Named addresses: John Smith <[email protected]> - Quoted names: "Smith, John" <[email protected]>

Working with Address Lists

Often you’ll need to handle multiple addresses, like in To:, Cc:, or Bcc: fields. That’s where ParseAddressList comes in:

func handleAddressList() error {
    // Common email header format
    list := `John Smith <[email protected]>, Alice <[email protected]>, "Bob, Jr" <[email protected]>`
    
    addresses, err := mail.ParseAddressList(list)
    if err != nil {
        return fmt.Errorf("parsing address list: %w", err)
    }
    
    for i, addr := range addresses {
        fmt.Printf("Address %d:\n", i+1)
        fmt.Printf("  Name: %q\n", addr.Name)
        fmt.Printf("  Email: %s\n", addr.Address)
    }
    
    return nil
}

Real-World Examples

Let’s look at some practical applications.

1. Email Address Validator

Here’s a robust email validator that uses net/mail:

type ValidationResult struct {
    Address     string
    IsValid     bool
    ErrorDetail string
}

func validateEmails(addresses []string) []ValidationResult {
    results := make([]ValidationResult, len(addresses))
    
    for i, addr := range addresses {
        result := ValidationResult{
            Address: addr,
        }
        
        // Try parsing
        parsed, err := mail.ParseAddress(addr)
        if err != nil {
            result.IsValid = false
            result.ErrorDetail = err.Error()
        } else {
            result.IsValid = true
            // Use the normalized address
            result.Address = parsed.Address
        }
        
        results[i] = result
    }
    
    return results
}

// Usage example
func demoValidation() {
    addresses := []string{
        "[email protected]",
        "John Smith <[email protected]>",
        "invalid@[email protected]",
        `"Smith, John" <[email protected]>`,
        "no-at-sign.example.com",
    }
    
    results := validateEmails(addresses)
    for _, r := range results {
        if r.IsValid {
            fmt.Printf("✓ %s is valid\n", r.Address)
        } else {
            fmt.Printf("✗ %s is invalid: %s\n", r.Address, r.ErrorDetail)
        }
    }
}

2. Address List Normalizer

When working with email systems, you often need to normalize address lists:

type AddressNormalizer struct {
    lowercase bool
    sort      bool
}

func NewAddressNormalizer(lowercase, sort bool) *AddressNormalizer {
    return &AddressNormalizer{
        lowercase: lowercase,
        sort:      sort,
    }
}

func (n *AddressNormalizer) NormalizeList(input string) (string, error) {
    // Parse the address list
    addresses, err := mail.ParseAddressList(input)
    if err != nil {
        return "", fmt.Errorf("parsing address list: %w", err)
    }
    
    // Normalize each address
    for i, addr := range addresses {
        if n.lowercase {
            addr.Address = strings.ToLower(addr.Address)
        }
        addresses[i] = addr
    }
    
    // Sort if requested
    if n.sort {
        sort.Slice(addresses, func(i, j int) bool {
            return addresses[i].Address < addresses[j].Address
        })
    }
    
    // Convert back to string
    var parts []string
    for _, addr := range addresses {
        parts = append(parts, addr.String())
    }
    
    return strings.Join(parts, ", "), nil
}

3. Header Address Extractor

When working with email headers, you often need to extract addresses from various fields:

type EmailAddresses struct {
    From    *mail.Address
    To      []*mail.Address
    Cc      []*mail.Address
    ReplyTo []*mail.Address
}

func ExtractAddresses(headers map[string][]string) (EmailAddresses, error) {
    var result EmailAddresses
    var err error
    
    // Handle From (should be single address)
    if from := headers["From"]; len(from) > 0 {
        result.From, err = mail.ParseAddress(from[0])
        if err != nil {
            return result, fmt.Errorf("parsing From address: %w", err)
        }
    }
    
    // Handle address lists
    if to := headers["To"]; len(to) > 0 {
        result.To, err = mail.ParseAddressList(to[0])
        if err != nil {
            return result, fmt.Errorf("parsing To addresses: %w", err)
        }
    }
    
    if cc := headers["Cc"]; len(cc) > 0 {
        result.Cc, err = mail.ParseAddressList(cc[0])
        if err != nil {
            return result, fmt.Errorf("parsing Cc addresses: %w", err)
        }
    }
    
    if replyTo := headers["Reply-To"]; len(replyTo) > 0 {
        result.ReplyTo, err = mail.ParseAddressList(replyTo[0])
        if err != nil {
            return result, fmt.Errorf("parsing Reply-To addresses: %w", err)
        }
    }
    
    return result, nil
}

Best Practices

  1. Always Use ParseAddress for Validation

    // Don't just check for @ symbol
    func isValidEmail(email string) bool {
       _, err := mail.ParseAddress(email)
       return err == nil
    }
  2. Handle Encoding Properly

    // When displaying names, remember they might contain UTF-8
    func formatAddress(addr *mail.Address) string {
       if addr.Name != "" {
           return fmt.Sprintf("%s <%s>", addr.Name, addr.Address)
       }
       return addr.Address
    }
  3. Use String() Method

    // Let the package handle proper quoting
    addr := &mail.Address{
       Name:    "Smith, John",
       Address: "[email protected]",
    }
    formatted := addr.String() // Will properly quote the name

Common Pitfalls

  1. Assuming Single Address in From Field ```go // Wrong from, _ := mail.ParseAddressList(headers.Get(“From”))

// Right from, _ := mail.ParseAddress(headers.Get(“From”))

2. **Not Handling Empty Lists**
   ```go
   func safeParseList(input string) []*mail.Address {
       if input == "" {
           return nil
       }
       addresses, err := mail.ParseAddressList(input)
       if err != nil {
           return nil
       }
       return addresses
   }
  1. Forgetting About Quotes ```go // This might not work as expected name := “Smith, John” email := “[email protected]” raw := fmt.Sprintf(“%s <%s>“, name, email)

// Better approach addr := &mail.Address{ Name: name, Address: email, } raw = addr.String() ```

What’s Next?

In our next article, we’ll dive into message handling with net/mail, including how to parse and work with entire email messages, not just addresses. We’ll see how to handle headers, bodies, and more complex email structures.

Until then, keep experimenting with the address parser! It’s a fundamental building block for any email-handling code in Go.