website/pkg/mail/mail.go
2024-05-20 13:29:02 -07:00

110 lines
3.0 KiB
Go

// Package mail provides e-mail sending faculties.
package mail
import (
"bytes"
"errors"
"fmt"
"html/template"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/encryption"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/redis"
"github.com/microcosm-cc/bluemonday"
"gopkg.in/gomail.v2"
)
// Message configuration.
type Message struct {
To string
ReplyTo string
Subject string
Template string // path relative to the templates dir, e.g. "email/verify_email.html"
Data map[string]interface{}
}
// LockSending emails to the same address within 24 hours, e.g.: on the signup form to reduce chance for spam abuse.
//
// Call this before calling Send() if you want to throttle the sending. This function will put a key in Redis on
// the first call and return nil; on subsequent calls, if the key still remains, it will return an error.
func LockSending(namespace, email string, expires time.Duration) error {
var key = fmt.Sprintf("mail/lock-sending/%s/%s", namespace, encryption.Hash([]byte(email)))
// See if we have already locked it.
if redis.Exists(key) {
return errors.New("email was in the lock-sending queue")
}
redis.Set(key, email, expires)
return nil
}
// Send an email.
func Send(msg Message) error {
conf := config.Current.Mail
// Verify configuration.
if !conf.Enabled {
return errors.New(
"Email sending is not configured for this app. Please contact the website administrator about this error.",
)
} else if conf.Host == "" || conf.Port == 0 || conf.From == "" {
return errors.New(
"Email settings are misconfigured for this app. Please contact the website administrator about this error.",
)
}
// Get and render the template to HTML.
var html bytes.Buffer
tmpl, err := template.New(msg.Template).ParseFiles(config.TemplatePath + "/" + msg.Template)
if err != nil {
return err
}
// Execute the template.
err = tmpl.ExecuteTemplate(&html, "content", msg)
if err != nil {
return fmt.Errorf("Mail template execute error: %s", err)
}
// Condense the HTML down into the plaintext version.
rawLines := strings.Split(
bluemonday.StrictPolicy().Sanitize(html.String()),
"\n",
)
var lines []string
for _, line := range rawLines {
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
lines = append(lines, line)
}
plaintext := strings.Join(lines, "\n\n")
// Prepare the e-mail!
m := gomail.NewMessage()
m.SetHeader("From", fmt.Sprintf("%s <%s>", config.Title, conf.From))
m.SetHeader("To", msg.To)
if msg.ReplyTo != "" {
m.SetHeader("Reply-To", msg.ReplyTo)
}
m.SetHeader("Subject", msg.Subject)
m.SetBody("text/plain", plaintext)
m.AddAlternative("text/html", html.String())
// Deliver asynchronously.
log.Info("mail.Send: %s (%s) to %s", msg.Subject, msg.Template, msg.To)
d := gomail.NewDialer(conf.Host, conf.Port, conf.Username, conf.Password)
go func() {
if err := d.DialAndSend(m); err != nil {
log.Error("mail.Send: %s", err.Error())
}
}()
return nil
}