164 lines
3.4 KiB
Go
164 lines
3.4 KiB
Go
package mailer
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/wneessen/go-mail"
|
|
|
|
ht "html/template"
|
|
tt "text/template"
|
|
)
|
|
|
|
//go:embed "templates"
|
|
var templateFS embed.FS
|
|
|
|
// Mailer renders embedded templates and delivers the resulting message.
|
|
type Mailer struct {
|
|
client *mail.Client
|
|
mode Mode
|
|
filePath string
|
|
sender string
|
|
fileMu sync.Mutex
|
|
}
|
|
|
|
// Mode selects the delivery backend used by a Mailer.
|
|
type Mode string
|
|
|
|
const (
|
|
// ModeSMTP sends messages through an SMTP server.
|
|
ModeSMTP Mode = "smtp"
|
|
// ModeFile appends rendered messages to a local development file.
|
|
ModeFile Mode = "file"
|
|
)
|
|
|
|
// Config contains SMTP or development-file delivery settings.
|
|
type Config struct {
|
|
Mode Mode
|
|
Host string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
Sender string
|
|
FilePath string
|
|
}
|
|
|
|
// New validates config and creates a Mailer.
|
|
func New(config Config) (*Mailer, error) {
|
|
mailer := &Mailer{
|
|
mode: config.Mode,
|
|
filePath: config.FilePath,
|
|
sender: config.Sender,
|
|
}
|
|
|
|
switch config.Mode {
|
|
case ModeSMTP:
|
|
client, err := mail.NewClient(
|
|
config.Host,
|
|
mail.WithSMTPAuth(mail.SMTPAuthLogin),
|
|
mail.WithPort(config.Port),
|
|
mail.WithUsername(config.Username),
|
|
mail.WithPassword(config.Password),
|
|
mail.WithTimeout(5*time.Second),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mailer.client = client
|
|
case ModeFile:
|
|
if config.FilePath == "" {
|
|
return nil, errors.New("mailer: file path must not be empty in file mode")
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("mailer: unsupported mode %q", config.Mode)
|
|
}
|
|
|
|
return mailer, nil
|
|
}
|
|
|
|
// Send renders templateFile with data and delivers it to recipient.
|
|
func (m *Mailer) Send(recipient string, templateFile string, data any) error {
|
|
textTmpl, err := tt.New("").ParseFS(templateFS, "templates/"+templateFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
subject := new(bytes.Buffer)
|
|
err = textTmpl.ExecuteTemplate(subject, "subject", data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
plainBody := new(bytes.Buffer)
|
|
err = textTmpl.ExecuteTemplate(plainBody, "plainBody", data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
htmlTmpl, err := ht.New("").ParseFS(templateFS, "templates/"+templateFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
htmlBody := new(bytes.Buffer)
|
|
err = htmlTmpl.ExecuteTemplate(htmlBody, "htmlBody", data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
msg := mail.NewMsg()
|
|
|
|
err = msg.To(recipient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = msg.From(m.sender)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
msg.Subject(subject.String())
|
|
msg.SetBodyString(mail.TypeTextPlain, plainBody.String())
|
|
msg.AddAlternativeString(mail.TypeTextHTML, htmlBody.String())
|
|
|
|
if m.mode == ModeFile {
|
|
return m.appendToFile(msg)
|
|
}
|
|
|
|
return m.client.DialAndSend(msg)
|
|
}
|
|
|
|
func (m *Mailer) appendToFile(msg *mail.Msg) error {
|
|
var content bytes.Buffer
|
|
if _, err := msg.WriteTo(&content); err != nil {
|
|
return fmt.Errorf("mailer: format message: %w", err)
|
|
}
|
|
|
|
m.fileMu.Lock()
|
|
defer m.fileMu.Unlock()
|
|
|
|
file, err := os.OpenFile(m.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("mailer: open output file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
if _, err = file.WriteString("\n=== gardomatic mail ===\n"); err != nil {
|
|
return fmt.Errorf("mailer: append separator: %w", err)
|
|
}
|
|
if _, err = content.WriteTo(file); err != nil {
|
|
return fmt.Errorf("mailer: append message: %w", err)
|
|
}
|
|
if _, err = file.WriteString("\n"); err != nil {
|
|
return fmt.Errorf("mailer: finish message: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|