App

Mailers

Render mailer templates and keep message delivery behind a service boundary.

By Guillermo Alvarez - Published - Updated

App-owned service

Mail is an application service. GoLazy provides rendering and delivery boundaries, but the app decides which mailers exist, how delivery is configured, and which controllers or jobs call them.

Wire the mailer in init/dependencies.go, then expose application operations through typed context helpers just like any other dependency.

Mailer setup

golazy.dev/lazymailer renders mail through the same view renderer used by controllers. Delivery is a separate interface, so SMTP, memory inboxes, and provider-specific transports can be swapped without changing templates.

func Dependencies(deps *lazydeps.Scope) error {
    _, err := lazydeps.Service(deps, "mailer", func(ctx context.Context) (
        context.Context,
        *lazymailer.Mailer,
        error,
        context.CancelFunc,
    ) {
        registry := lazymailer.NewRegistry("default", map[string]lazymailer.Delivery{
            "default": lazymailer.SMTPDelivery{
                Addr: os.Getenv("SMTP_ADDR"),
                Auth: smtp.PlainAuth(
                    "",
                    os.Getenv("SMTP_USER"),
                    os.Getenv("SMTP_PASSWORD"),
                    os.Getenv("SMTP_HOST"),
                ),
            },
        })

        mailer, err := lazymailer.New(ctx, registry)
        if err != nil {
            return ctx, nil, fmt.Errorf("initialize mailer: %w", err), nil
        }

        return lazymailer.WithContext(ctx, mailer), mailer, nil, nil
    })
    return err
}

Use lazymailer.MemoryDelivery in tests or development when you want to assert which messages would be sent.

Application mailer types

Put mailer types under app/mailers. A concrete mailer embeds or stores a lazymailer.Base, sets template variables, and calls Mail.

package notices

type NoticeMailer struct {
    base lazymailer.Base
}

func New(ctx context.Context) (*NoticeMailer, error) {
    base, err := lazymailer.NewBase(ctx, "notice_mailer", lazymailer.Defaults{
        From:     lazymailer.MustParseAddress("GoLazy <[email protected]>"),
        Delivery: "default",
        Layout:   "mailer",
    })
    if err != nil {
        return nil, err
    }
    return &NoticeMailer{base: base}, nil
}

func (m *NoticeMailer) Welcome(to lazymailer.Address, name string) error {
    m.base.Set("name", name)
    return m.base.Mail(lazymailer.Options{
        Action:  "welcome",
        To:      []lazymailer.Address{to},
        Subject: "Welcome",
    })
}

The controller should call an app operation such as Welcome; it should not build MIME messages or know which delivery transport is active.

Mailer views

Mailer templates live next to normal views. The controller name passed to NewBase selects the view directory, and the action selects the template name.

app/views/layouts/mailer.text.tpl
app/views/layouts/mailer.html.tpl
app/views/notice_mailer/welcome.text.tpl
app/views/notice_mailer/welcome.html.tpl

When both text and HTML templates exist, GoLazy builds a multipart alternative message. If only one format exists, the message uses that body.

Stored files, application assets, object storage, and generated media variants now live in the Media guide. Keep mail delivery and media storage as separate app services unless your product operation genuinely needs both, such as sending an email with a generated preview link.