Sitemap

Deep Dive into net/mail Message Handling 1/2

5 min readJan 12, 2025

--

Hey there! In our previous article, we explored email address handling in Go’s net/mail package. Today, we’re diving into message handling — how to parse, read, and work with entire email messages. This is where things get interesting because email messages can be quite complex.

Understanding Message Structure

Let’s start with the basics of the Message type:

package main

import (
"fmt"
"net/mail"
"strings"
)

func demoBasicMessage() {
// A simple email message
rawEmail := `From: sender@example.com
To: recipient@example.com
Subject: Hello, World!
Date: Thu, 4 Jan 2024 12:00:00 -0700

This is the message body.
`

r := strings.NewReader(rawEmail)
msg, err := mail.ReadMessage(r)
if err != nil {
panic(err)
}

fmt.Printf("From: %s\n", msg.Header.Get("From"))
fmt.Printf("Subject: %s\n", msg.Header.Get("Subject"))
}

Working with Headers

Email headers are key-value pairs with some special properties. Let’s explore how to handle them:

type HeaderAnalysis struct {
From *mail.Address
To []*mail.Address
Subject string
MessageID string
Date time.Time
ContentType string
Encoding string
}

func analyzeHeaders(msg *mail.Message) (HeaderAnalysis, error) {
var analysis HeaderAnalysis
var err error

// Parse From address
if from := msg.Header.Get("From"); from != "" {
analysis.From, err = mail.ParseAddress(from)
if err != nil {
return analysis, fmt.Errorf("parsing From: %w", err)
}
}

// Parse To addresses
if to := msg.Header.Get("To"); to != "" {
analysis.To, err = mail.ParseAddressList(to)
if err != nil {
return analysis, fmt.Errorf("parsing To: %w", err)
}
}

// Get subject
analysis.Subject = msg.Header.Get("Subject")

// Get Message-ID
analysis.MessageID = msg.Header.Get("Message-ID")

// Parse date
if dateStr := msg.Header.Get("Date"); dateStr != "" {
analysis.Date, err = msg.Header.Date()
if err != nil {
return analysis, fmt.Errorf("parsing date: %w", err)
}
}

// Get content type
analysis.ContentType = msg.Header.Get("Content-Type")
analysis.Encoding = msg.Header.Get("Content-Transfer-Encoding")

return analysis, nil
}

Real-World Examples

1. Email Message Parser

Here’s a comprehensive message parser that handles common email formats:

type EmailContent struct {
Headers mail.Header
TextBody string
HTMLBody string
Attachments []Attachment
}

type Attachment struct {
Filename string
ContentType string
Data []byte
}

func ParseEmailMessage(r io.Reader) (*EmailContent, error) {
msg, err := mail.ReadMessage(r)
if err != nil {
return nil, fmt.Errorf("reading message: %w", err)
}

content := &EmailContent{
Headers: msg.Header,
}

// Get Content-Type and boundary
mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
if err != nil {
// Default to text/plain if header is missing or invalid
mediaType = "text/plain"
}

if strings.HasPrefix(mediaType, "multipart/") {
err = handleMultipartMessage(msg.Body, params["boundary"], content)
if err != nil {
return nil, fmt.Errorf("handling multipart: %w", err)
}
} else {
// Single part message
body, err := io.ReadAll(msg.Body)
if err != nil {
return nil, fmt.Errorf("reading body: %w", err)
}

if strings.HasPrefix(mediaType, "text/html") {
content.HTMLBody = string(body)
} else {
content.TextBody = string(body)
}
}

return content, nil
}

func handleMultipartMessage(reader io.Reader, boundary string, content *EmailContent) error {
mr := multipart.NewReader(reader, boundary)

for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading part: %w", err)
}

mediaType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
if err != nil {
continue // Skip parts with invalid Content-Type
}

// Handle nested multipart messages
if strings.HasPrefix(mediaType, "multipart/") {
err = handleMultipartMessage(part, params["boundary"], content)
if err != nil {
return err
}
continue
}

data, err := io.ReadAll(part)
if err != nil {
return fmt.Errorf("reading part data: %w", err)
}

// Handle different content types
switch {
case mediaType == "text/plain":
content.TextBody = string(data)

case mediaType == "text/html":
content.HTMLBody = string(data)

default:
// Treat as attachment
filename := part.FileName()
if filename == "" {
filename = "unnamed_attachment"
}

content.Attachments = append(content.Attachments, Attachment{
Filename: filename,
ContentType: mediaType,
Data: data,
})
}
}

return nil
}

2. Header Validator

A tool to validate and normalize email headers:

type HeaderValidation struct {
Field string
Status string
Details string
}

type HeaderValidator struct {
rules map[string]func(string) error
}

func NewHeaderValidator() *HeaderValidator {
v := &HeaderValidator{
rules: make(map[string]func(string) error),
}

// Add default validation rules
v.rules["From"] = validateSingleAddress
v.rules["To"] = validateAddressList
v.rules["Cc"] = validateAddressList
v.rules["Bcc"] = validateAddressList
v.rules["Message-ID"] = validateMessageID
v.rules["Date"] = validateDate

return v
}

func (v *HeaderValidator) ValidateHeaders(headers mail.Header) []HeaderValidation {
var results []HeaderValidation

for field, rule := range v.rules {
value := headers.Get(field)
result := HeaderValidation{
Field: field,
}

if value == "" {
if field == "From" || field == "To" || field == "Date" {
result.Status = "Error"
result.Details = "Required field missing"
} else {
result.Status = "Info"
result.Details = "Optional field missing"
}
} else {
if err := rule(value); err != nil {
result.Status = "Error"
result.Details = err.Error()
} else {
result.Status = "Valid"
}
}

results = append(results, result)
}

return results
}

func validateSingleAddress(value string) error {
_, err := mail.ParseAddress(value)
return err
}

func validateAddressList(value string) error {
_, err := mail.ParseAddressList(value)
return err
}

func validateMessageID(value string) error {
if !strings.HasPrefix(value, "<") || !strings.HasSuffix(value, ">") {
return fmt.Errorf("message ID must be enclosed in angle brackets")
}
return nil
}

func validateDate(value string) error {
_, err := mail.ParseDate(value)
return err
}

3. Message Formatter

A tool to help construct properly formatted email messages:

type MessageBuilder struct {
header mail.Header
body bytes.Buffer
writers []func(io.Writer) (io.Writer, error)
}

func NewMessageBuilder() *MessageBuilder {
return &MessageBuilder{
header: make(mail.Header),
}
}

func (mb *MessageBuilder) SetFrom(addr *mail.Address) *MessageBuilder {
mb.header["From"] = []string{addr.String()}
return mb
}

func (mb *MessageBuilder) SetTo(addrs []*mail.Address) *MessageBuilder {
var strs []string
for _, addr := range addrs {
strs = append(strs, addr.String())
}
mb.header["To"] = []string{strings.Join(strs, ", ")}
return mb
}

func (mb *MessageBuilder) SetSubject(subject string) *MessageBuilder {
mb.header["Subject"] = []string{subject}
return mb
}

func (mb *MessageBuilder) SetContentType(contentType string) *MessageBuilder {
mb.header["Content-Type"] = []string{contentType}
return mb
}

func (mb *MessageBuilder) AddTextBody(text string) *MessageBuilder {
mb.body.WriteString(text)
return mb
}

func (mb *MessageBuilder) Build() ([]byte, error) {
var buf bytes.Buffer

// Set date if not already set
if mb.header.Get("Date") == "" {
mb.header["Date"] = []string{time.Now().Format(time.RFC1123Z)}
}

// Write headers
for k, v := range mb.header {
for _, val := range v {
fmt.Fprintf(&buf, "%s: %s\n", k, val)
}
}

// Separate headers from body
buf.WriteString("\n")

// Write body through any encoders
w := io.Writer(&buf)
for _, wf := range mb.writers {
var err error
w, err = wf(w)
if err != nil {
return nil, fmt.Errorf("setting up writer: %w", err)
}
}

_, err := io.Copy(w, &mb.body)
if err != nil {
return nil, fmt.Errorf("writing body: %w", err)
}

return buf.Bytes(), nil
}

Best Practices

  1. Always Check Content-Type
func getContentType(msg *mail.Message) string {
contentType := msg.Header.Get("Content-Type")
if contentType == "" {
return "text/plain" // Default as per RFC
}
return contentType
}

2. Handle Character Encodings

func decodeHeader(header string) string {
decoded, err := mime.WordDecoder{}.DecodeHeader(header)
if err != nil {
return header // Return original if decoding fails
}
return decoded
}

3.Parse Dates Carefully

func getMessageDate(msg *mail.Message) (time.Time, error) {
date, err := msg.Header.Date()
if err != nil {
// Try parsing with alternative formats
dateStr := msg.Header.Get("Date")
for _, format := range alternativeDateFormats {
if t, err := time.Parse(format, dateStr); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unable to parse date: %w", err)
}
return date, nil
}

What’s Next?

In our final article, we’ll look at how to build complete email applications using net/mail, including how to handle MIME types, attachments, and integration with SMTP libraries.

Get Rez Moss’s stories in your inbox

Join Medium for free to get updates from this writer.

Until then, keep exploring message handling! Remember that email messages can be complex beasts, so always test with a variety of real-world examples.

--

--

Rez Moss
Rez Moss

Written by Rez Moss

0 followers

🛠️ Building scalable online businesses & cloud solutions https://zlnk.me/rezmoss