package postcard import ( "context" "fmt" "strings" "git.golem15.com/golem15/summercms/backpack" "git.golem15.com/golem15/summercms/pact" ) // Message is the Send contract for plugin callers. type Message struct { Template string To []string Cc []string Bcc []string ReplyTo string Vars map[string]any Subject string } // Mailer sends a registered template through the configured driver. type Mailer interface { Send(ctx context.Context, msg Message) error } // Options configure an app-scoped mailer. type Options struct { From string CSS string BrandCSS string } type mailer struct { catalog *Catalog driver Driver opts Options } // NewMailer returns a mailer that renders from cat and delivers through driver. func NewMailer(cat *Catalog, driver Driver, opts Options) Mailer { if cat == nil { cat = NewCatalog() } return &mailer{catalog: cat, driver: driver, opts: opts} } // Send renders msg.Template and delivers it. It performs no locale selection; // the caller must pass the full dotted name, including any -en suffix. func (m *mailer) Send(ctx context.Context, msg Message) error { if m == nil { return fmt.Errorf("postcard: mailer is nil") } if m.driver == nil { return fmt.Errorf("postcard: driver is nil") } if msg.Template == "" { return fmt.Errorf("postcard: template is empty") } if len(msg.To) == 0 && len(msg.Cc) == 0 && len(msg.Bcc) == 0 { return fmt.Errorf("postcard: no recipients") } rendered, err := m.catalog.render(msg, renderOptions{ css: m.opts.CSS, brandCSS: m.opts.BrandCSS, }) if err != nil { return err } rendered.From = m.opts.From if err := m.driver.Send(ctx, rendered); err != nil { return fmt.Errorf("postcard: %w", err) } return nil } // Activate publishes an app-scoped Mailer before plugin Boot. Catalog files // are validated later at each plugin's Boot transition. func Activate[P interface{ ID() string }](app *backpack.App, _ []P) error { if app == nil { return fmt.Errorf("postcard: app is nil") } opts := Options{} if app.Config != nil { opts.From = strings.TrimSpace(app.Config.String("mail.from")) opts.CSS = app.Config.String("mail.css") opts.BrandCSS = app.Config.String("mail.brandCss") } m := &mailer{ catalog: NewCatalog(), driver: NewMemoryDriver(), opts: opts, } if err := app.Publish[Mailer](m); err != nil { return fmt.Errorf("postcard: %w", err) } return nil } // BootPlugin registers a plugin's declared mail templates and layouts. // Missing files and unknown layout aliases fail with the dotted name. func BootPlugin[P interface{ ID() string }](app *backpack.App, p P) error { hm, ok := any(p).(pact.HasMailTemplates) if !ok { return nil } got, ok := app.Lookup[Mailer]() if !ok { return fmt.Errorf("postcard: mailer not published") } m, ok := got.(*mailer) if !ok || m == nil || m.catalog == nil { return fmt.Errorf("postcard: mailer not published") } return m.catalog.Register(p.ID(), hm.MailTemplatesFS(), hm.MailTemplates(), hm.MailLayouts()) }