Initial commit
CI / test (push) Canceled after 0s

This commit is contained in:
2026-09-12 22:22:17 +02:00
commit 904d14b64c
314 changed files with 31884 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
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
}