email.go

68 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
// Package email sends templated transactional email. It is a provider-shaped
// package like platform and the database engines: one Emailer interface, a Base that renders
// templates from an embedded filesystem, and concrete senders (Congo, Resend,
// Logger) behind it. It has no dependency on application — an app wires an Emailer
// into whichever controller needs it, rather than the MVC core carrying email.
package email

import (
	"bytes"
	"embed"
	"errors"
	"fmt"
	"html/template"
	"io/fs"
	"log"
	"path/filepath"
	"strings"
)

// Emailer sends a templated email.
type Emailer interface {
	Send(to, subject, templateName string, data map[string]any) error
}

// Base provides template rendering for emailer implementations. Embed it and call
// Init from the constructor.
type Base struct {
	emails *template.Template
}

// Init parses the email templates under the "emails" directory of the embedded
// filesystem, with an optional FuncMap.
func (b *Base) Init(emails embed.FS, funcs template.FuncMap) {
	b.emails = template.New("")
	if funcs != nil {
		b.emails = b.emails.Funcs(funcs)
	}
	err := fs.WalkDir(emails, "emails", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() || !strings.HasSuffix(path, ".html") {
			return nil
		}
		content, err := emails.ReadFile(path)
		if err != nil {
			return err
		}
		_, err = b.emails.New(filepath.Base(path)).Parse(string(content))
		return err
	})
	if err != nil {
		log.Printf("Warning: failed to parse email templates: %v", err)
		b.emails = template.New("")
	}
}

// Render executes a named email template to a string.
func (b *Base) Render(name string, data map[string]any) (string, error) {
	if b.emails == nil {
		return "", errors.New("email templates not initialized")
	}
	var buf bytes.Buffer
	if err := b.emails.ExecuteTemplate(&buf, name, data); err != nil {
		return "", fmt.Errorf("execute template %s: %w", name, err)
	}
	return buf.String(), nil
}