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() }