Julien Sellier

Last update: 2025-12-13

Sending Emails in Go

Here's a simple implementation of a email/SMTP helper package in Go:

package mailx

import (
	"bytes"
	"log/slog"
	"net/mail"
	"net/smtp"
	"strings"
	"unicode"
)

type Emailer interface {
	Email(*Message) error
}

type Message struct {
	From          mail.Address
	To            []string
	Subject       string
	PlainTextBody string
}

type MultiEmailer struct {
	Emailers []Emailer
	Logger   *slog.Logger
}

func (e MultiEmailer) Email(msg *Message) (err error) {
	for _, v := range e.Emailers {
		err = v.Email(msg)
		if err != nil {
			e.Logger.Error("send email", "error", err)
			continue
		}
		break
	}
	return err
}

type SMTPEmailer struct {
	RelayAddress string // Ex: "smtp.example.org:587"
	AuthHost     string // Ex: "smtp.example.org".
	AuthUsername string // Ex: "myaccount@example.org".
	AuthPassword string // Ex: "123456".
}

func (e *SMTPEmailer) Email(msg *Message) error {
	auth := smtp.PlainAuth("", e.AuthUsername, e.AuthPassword, e.AuthHost)
	return smtp.SendMail(e.RelayAddress, auth, msg.From.Address, msg.To, DATA(msg))
}

// Generates a SMTP DATA string.
func DATA(e *Message) []byte {
	d := &bytes.Buffer{}
	d.WriteString("From: " + e.From.String() + "\r\n")
	d.WriteString("To: " + strings.Join(e.To, "; ") + "\r\n")
	d.WriteString("Subject: " + e.Subject + "\r\n")
	d.WriteString("MIME-Version: " + "1.0" + "\r\n")
	d.WriteString("Content-Type: " + "text/plain" + "\r\n")
	d.WriteString("\r\n")
	d.WriteString(e.PlainTextBody)
	d.WriteString("\r\n")
	return d.Bytes()
}

type MockEmailer struct {
	Logger *slog.Logger // Mock emails will be logged here.
	Err    error        // To mock failed send.
}

func (e *MockEmailer) Email(msg *Message) error {
	e.Logger.Info("mock email",
		"error", e.Err,
		"from_name", msg.From.Name,
		"from_address", msg.From.Address,
		"to", msg.To,
		"subject", msg.Subject,
		"body", msg.PlainTextBody,
	)
	return e.Err
}

// Sanitizes plain-text body for usage in DATA SMTP command.
func Sanitize(v string) (safe string) {
	v = strings.ReplaceAll(v, "\r", "")
	for _, c := range v {
		// Prevent CR (a malicious user could trigger end of DATA with '\r\n' sequence).
		if c == '\r' {
			continue
		}
		// Prevent non-graphic characters (a malicious user could insert ANSI escape sequences).
		if !unicode.IsGraphic(c) && c != '\n' {
			continue
		}
		safe += string(c)
	}
	return safe
}