-
ale authored
We're using language-specific Markdown templates to allow simultaneous generation of text and HTML for fancy multipart/alternative emails. The package also supports PGP/MIME for signatures (no encryption yet).
ale authoredWe're using language-specific Markdown templates to allow simultaneous generation of text and HTML for fancy multipart/alternative emails. The package also supports PGP/MIME for signatures (no encryption yet).
mail.go 3.18 KiB
package mail
import (
"bytes"
"errors"
"fmt"
"net"
"net/mail"
"net/smtp"
"strings"
"time"
"git.autistici.org/ai3/go-common/mail/message"
"git.autistici.org/ai3/go-common/mail/pgp"
)
var userAgent = "go-mailer/0.1"
type Config struct {
SenderName string `yaml:"sender_name"`
SenderAddr string `yaml:"sender_addr"`
SigningKeyFile string `yaml:"signing_key_file"`
SigningKeyID string `yaml:"signing_key_id"`
SMTP struct {
Server string `yaml:"server"`
AuthUser string `yaml:"auth_user"`
AuthPassword string `yaml:"auth_password"`
} `yaml:"smtp"`
}
type Mailer struct {
sender *mail.Address
senderDomain string
middleware message.MiddlewareList
smtpServer string
smtpAuth smtp.Auth
}
func New(config *Config) (*Mailer, error) {
if config.SenderAddr == "" {
return nil, errors.New("sender_addr must not be empty")
}
senderDomainParts := strings.Split(config.SenderAddr, "@")
senderDomain := senderDomainParts[len(senderDomainParts)-1]
sender := &mail.Address{
Name: config.SenderName,
Address: config.SenderAddr,
}
// Load the signing key, if specified.
var mwl message.MiddlewareList
if config.SigningKeyFile != "" {
signer, err := pgp.NewSigner(config.SigningKeyFile, config.SigningKeyID, sender)
if err != nil {
return nil, err
}
mwl.Add(signer)
}
// Parse SMTP authentication params.
var smtpAuth smtp.Auth
if config.SMTP.AuthUser != "" {
// The hostname is used by net/smtp to validate the TLS certificate.
hostname := config.SMTP.Server
if h, _, err := net.SplitHostPort(hostname); err == nil {
hostname = h
}
smtpAuth = smtp.PlainAuth("", config.SMTP.AuthUser, config.SMTP.AuthPassword, hostname)
}
return &Mailer{
sender: sender,
senderDomain: senderDomain,
middleware: mwl,
smtpServer: config.SMTP.Server,
smtpAuth: smtpAuth,
}, nil
}
func (m *Mailer) WithEnvelope(msg *message.Part, rcpt, subject string) (*message.Part, error) {
rcptAddr := mail.Address{Address: rcpt}
hdr := msg.Header
hdr.Set("From", m.sender.String())
hdr.Set("To", rcptAddr.String())
hdr.Set("Subject", subject)
hdr.Set("User-Agent", userAgent)
hdr.Set("MIME-Version", "1.0")
hdr.Set("Message-ID", fmt.Sprintf("<%s>", m.randomMessageID()))
hdr.Set("Date", currentTimeFn().Format(time.RFC1123Z))
var err error
msg, err = m.middleware.Process(msg)
if err != nil {
return nil, err
}
return msg, nil
}
func (m *Mailer) Render(msg *message.Part) (body string, err error) {
var buf bytes.Buffer
err = msg.Render(&buf)
if err == nil {
body = buf.String()
}
return
}
func (m *Mailer) randomMessageID() string {
// Re-use randomBoundary.
return fmt.Sprintf("%s@%s", message.RandomBoundaryFn(), m.senderDomain)
}
func (m *Mailer) Send(msg *message.Part, rcpt, subject string) error {
var err error
msg, err = m.WithEnvelope(msg, rcpt, subject)
if err != nil {
return err
}
var buf bytes.Buffer
if err := msg.Render(&buf); err != nil {
return err
}
return smtp.SendMail(m.smtpServer, m.smtpAuth, m.sender.Address, []string{rcpt}, buf.Bytes())
}
// Let us stub out the time function for testing.
var currentTimeFn func() time.Time = currentTime
func currentTime() time.Time {
return time.Now().UTC()
}