actionmailer

package module
v0.0.0-...-038d457 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 14 Imported by: 0

README

go-ruby-actionmailer/actionmailer

actionmailer — go-ruby-actionmailer

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the foundation of Rails' Action Mailer — the framework that composes and delivers email from mailer classes. Faithful to MRI / Rails 4.0.5 semantics for message composition, multipart MIME assembly, delivery methods, and the interceptor/observer hooks — without any Ruby runtime.

It is the Action Mailer backend for go-embedded-ruby, but a standalone, reusable module. It builds on two siblings:

  • go-ruby-mail — the Mail gem: this package assembles a Mail::Message and lets go-ruby-mail fold headers, encode words, and lay out MIME boundaries (the on-the-wire byte encoding).
  • go-ruby-activesupportpresent? / blank? option handling and reverse_merge header precedence.

Two seams

Everything interpreter-independent lives here as pure Go. Two things are host seams:

  1. Body rendering is a RenderBody callback. In Rails this is Action View resolving welcome.text.erb / welcome.html.erb; here the host supplies the rendered text/HTML per format and this package assembles the MIME structure (multipart/alternative → related → mixed) around it. Returning ErrNoTemplate skips a format, exactly as a missing template does. A ready-made View — built on go-ruby-actionview — supplies that callback with real template lookup (locale fallback, implicit text/HTML pairing) and layout wrapping; only the final ERB evaluation stays a seam (wired by the host to go-ruby-erb + a Ruby runtime), so the package stays CGO- and runtime-free.
  2. Delivery transport is the DeliveryMethod interface. SMTPDelivery performs the send through an injectable net/smtp.SendMail-shaped function, so tests never open a socket.

Install

go get github.com/go-ruby-actionmailer/actionmailer

Usage

package main

import (
	"fmt"

	am "github.com/go-ruby-actionmailer/actionmailer"
)

func main() {
	m := am.New("UserMailer")
	m.Default("from", "notifications@example.com")
	m.UseTestDelivery() // append to m.Deliveries instead of sending

	// The body-rendering seam (Action View stands in here).
	m.RenderBody = func(mailer, action, format string, locals map[string]any) (string, error) {
		switch format {
		case "text":
			return fmt.Sprintf("Welcome, %s!", locals["name"]), nil
		case "html":
			return fmt.Sprintf("<h1>Welcome, %s!</h1>", locals["name"]), nil
		}
		return "", am.ErrNoTemplate
	}

	m.Register("welcome", func(mm *am.Mailer, params ...any) error {
		name := params[0].(string)
		mm.Attachments().Set("terms.pdf", []byte("%PDF-1.4 ..."))
		return mm.Mail(am.MailOptions{
			To:      []string{"ada@example.com"},
			Subject: "Welcome",
			Locals:  map[string]any{"name": name},
		})
	})

	// Mirrors UserMailer.welcome("Ada").deliver_now
	if err := m.Process("welcome", "Ada").DeliverNow(); err != nil {
		panic(err)
	}
	fmt.Println(m.Deliveries[0].Encoded())
}

The composed message above is multipart/mixed [ multipart/alternative [ text/plain, text/html ], terms.pdf ]. Add inline attachments with Attachments().SetInline(name, data) and the alternative is wrapped in a multipart/related carrying the Content-IDs.

API

// Mailer class configuration + action registry (subclass of ActionMailer::Base).
func New(name string) *Base
func (b *Base) Default(key, value string) *Base         // default from:, subject:, …
func (b *Base) Register(action string, fn Action) *Base // bind a mailer action
func (b *Base) Process(action string, params ...any) *MessageDelivery // Mailer.action(params)
func (b *Base) RegisterInterceptor(i Interceptor) *Base
func (b *Base) RegisterObserver(o Observer) *Base
func (b *Base) UseTestDelivery() *Base

// Class-level config fields: RenderBody, DeliveryMethod, PerformDeliveries,
// RaiseDeliveryErrors, Deliveries, Now, MessageIDGen, EnqueueJob.

// The body-rendering seam.
type RenderBody func(mailer, action, format string, locals map[string]any) (string, error)

// An action's body; call m.Mail(...) inside it.
type Action func(m *Mailer, params ...any) error

// Per-invocation mailer instance (self).
func (m *Mailer) Mail(opts MailOptions) error       // mail(to:, from:, subject:, …)
func (m *Mailer) Headers(h map[string]string) *Mailer
func (m *Mailer) Attachments() *Attachments

type MailOptions struct {
	From                 string
	To, Cc, Bcc, ReplyTo []string
	Subject              string
	Date                 time.Time
	HasDate              bool
	Headers              map[string]string
	Body, ContentType    string   // explicit single-part body (skips RenderBody)
	Formats              []string // formats to render (default text, html)
	Locals               map[string]any
}

// attachments[name] = data / attachments.inline[name] = data
func (a *Attachments) Set(name string, data []byte) *Attachment
func (a *Attachments) SetInline(name string, data []byte) *Attachment
func (a *Attachments) Get(name string) *Attachment
func (a *Attachments) All() []*Attachment
func (a *Attachments) Inline() []*Attachment
func (a *Attachments) Regular() []*Attachment

// The lazy delivery proxy (ActionMailer::MessageDelivery).
func (d *MessageDelivery) Message() (*mail.Message, error) // .message
func (d *MessageDelivery) DeliverNow() error               // .deliver_now
func (d *MessageDelivery) DeliverLater() error             // .deliver_later (Active Job seam)

// Delivery methods.
type DeliveryMethod interface{ Deliver(m *mail.Message) error }
func NewTestDelivery(dst *[]*mail.Message) *TestDelivery // :test — append to a slice
func NewFileDelivery(location string) *FileDelivery      // :file — one file per recipient
func NewSMTPDelivery(addr string) *SMTPDelivery          // :smtp — net/smtp (injectable Send)
func NewSendmailDelivery() *SendmailDelivery             // :sendmail — pipe to /usr/sbin/sendmail -i -t

// i18n subject: mail(subject: …) omitted -> <mailer_scope>.<action>.subject.
type I18n struct { Locale string; Translations map[string]string }
func NewI18n(locale string) *I18n
func (i *I18n) Set(key, value string) *I18n // "user_mailer.welcome.subject" = "Welcome, %{name}!"

// Templates + layouts (see below).
type View struct { Store *TemplateStore; Eval EvalTemplate; Locale, Layout string; Context *actionview.Context }
func (v *View) RenderBody() RenderBody
func (b *Base) UseView(v *View) *Base
func (b *Base) UseSendmail() *Base

// Interceptors & observers (register_interceptor / register_observer).
type Interceptor interface{ DeliveringEmail(m *mail.Message) }
type Observer interface{ DeliveredEmail(m *mail.Message) }

MIME assembly

Mail builds the tree ActionMailer produces:

  • rendered parts → a single leaf, or multipart/alternative when more than one;
  • inline attachments wrap the body in multipart/related (carrying Content-IDs);
  • regular attachments wrap everything in multipart/mixed.

Boundaries are produced by the overridable GenerateBoundary (deterministic by default for reproducible output); inline Content-IDs by GenerateContentID. Attachment media types are guessed from the filename extension and overridable on the returned *Attachment. Header fields are emitted in the Mail gem's canonical FIELD_ORDER, multipart containers carry Content-Transfer-Encoding: 7bit (and the root a charset=UTF-8), and text parts negotiate their transfer encoding the way the gem does — 7bit for US-ASCII, else the lower-cost of quoted-printable and base64 — so the wire bytes match Mail::Message#encoded.

Templates & layouts

View turns a TemplateStore plus an EvalTemplate seam into a RenderBody, reusing go-ruby-actionview for the render pipeline and html-safety. It resolves the mailer's per-format templates (with locale fallback and implicit text/HTML pairing), evaluates each through the ERB seam, and wraps the result in the format's layout — exposing the rendered body to the layout as the html-safe yield local:

store := am.NewTemplateStore().
	Add("UserMailer", "welcome", "text", "Hi <%= name %>").
	Add("UserMailer", "welcome", "html", "<p>Hi <%= name %></p>").
	AddLayout("mailer", "html", "", "<body><%= yield %></body>")

m.UseView(&am.View{Store: store, Eval: myERBEval, Layout: "mailer"})

Delivery methods

:test (append to a slice), :file (one file per recipient), :smtp (net/smtp through an injectable send function), and :sendmail (pipe the encoded message to /usr/sbin/sendmail -i -t, with an injectable executor so tests never spawn a process). deliver_now delivers inline; deliver_later runs through the Active Job seam.

i18n subject

When an action calls mail(...) with no subject: (and no default subject:), the subject is looked up from the I18n store at <locale>.<mailer_scope>.<action>.subject (e.g. en.user_mailer.welcome.subject), with %{name} interpolations from MailOptions.SubjectVars and a humanised action name as the fallback — mirroring ActionMailer's default_i18n_subject.

Roadmap (deferred)

Implemented: mailer base + actions, message composition and multipart MIME assembly, attachments (regular + inline/CID), template lookup with locale fallback and layouts (View), the :test / :file / :smtp / :sendmail delivery methods, deliver_now / deliver_later, interceptors + observers, default options, i18n subject interpolation, and gem-faithful field ordering and text transfer-encoding negotiation.

Deferred to later passes:

  • The Rails engine & generators (rails g mailer), railtie configuration.
  • The full on-disk Action View template resolver — template inheritance, handler discovery and view-path search (the in-memory TemplateStore covers action/format/locale lookup; ERB evaluation stays a host seam).
  • Mailer previews (ActionMailer::Preview).
  • Inline-CSS / premailer for HTML parts.
  • The brand asset (banner) and MkDocs/mike docs site (org infra follow-up).

Tests & coverage

Deterministic, ruby-free tests hold line coverage at 100% — message composition (single-part, alternative, related, mixed, empty, attachments both regular and inline), every delivery method (via fakes/temp dirs and an injected SMTP sender — no real socket), the deliveries array, interceptors/observers, deliver_now / deliver_later (inline and via the Active Job seam), and all error branches. MIME output is asserted against the expected part structure and round-tripped through go-ruby-mail's encoder.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

A differential gem oracle (TestRubyOracleByteParity, skip-gated on Ruby + the actionmailer gem, so it never runs on a Ruby-less CI) composes the same messages with real Rails Action Mailer and asserts the go output matches Mail::Message#encoded byte-for-byte across single-part, alternative, mixed, related, fully-nested and non-ASCII scenarios — normalising only the non-deterministic Date / Message-ID / inline Content-ID and three semantically inert go-ruby-mail-vs-mail-gem encoder-policy details (parameter folding, boundary quoting, and the gem's empty MIME preamble line).

CGO-free, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes (Linux, macOS, Windows).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-actionmailer/actionmailer authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package actionmailer is a pure-Go (CGO-free) reimplementation of the foundation of Ruby on Rails' Action Mailer — the framework that composes and delivers email from mailer classes. It is faithful to MRI / Rails 4.0.5 semantics for message composition, multipart MIME assembly, delivery methods, and the interceptor/observer hooks, without any Ruby runtime.

What it is — and isn't

Composing a message (assembling headers, multipart/alternative bodies, attachments, and choosing a delivery method) is deterministic and interpreter-independent, so it lives here as pure Go. Two things are host seams rather than baked in:

  • Rendering a mail body from a template is delegated to a RenderBody callback. In Rails this is Action View resolving `welcome.text.erb` / `welcome.html.erb`; here the host (for example go-embedded-ruby wiring up go-ruby-actionview) supplies the rendered text/HTML for each format and this package assembles the MIME structure around it.
  • The on-the-wire byte encoding of a message is delegated to go-ruby-mail (the Mail gem): this package builds a *mail.Message and lets go-ruby-mail fold headers, encode words, and lay out boundaries.

API shape

New creates a Base — the analogue of a subclass of ActionMailer::Base — carrying class-level configuration (Base.Default params, the delivery method, perform/raise flags, interceptors, observers). Base.Register binds a named action to an Action closure; running it with Base.Process returns a MessageDelivery (the analogue of `MyMailer.welcome(user)`), whose MessageDelivery.DeliverNow / MessageDelivery.DeliverLater / MessageDelivery.Message mirror the Rails proxy.

Inside an action, the *Mailer receiver exposes Mailer.Mail (the `mail(to:, from:, subject:, …)` call), Mailer.Headers, and Mailer.Attachments (both regular and inline/content-id attachments).

Delivery methods

DeliveryMethod is the pluggable transport. TestDelivery appends to a slice (ActionMailer::Base.deliveries), FileDelivery writes one file per recipient, and SMTPDelivery speaks SMTP via net/smtp through an injectable send function so tests never open a socket.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoTemplate is returned by a [RenderBody] seam to indicate that no
	// template exists for the requested format, so that format is skipped.
	ErrNoTemplate = errors.New("actionmailer: no template for format")

	// ErrNoRenderBody is returned by [Mailer.Mail] when a body must be rendered
	// but no [RenderBody] seam is configured on the [Base].
	ErrNoRenderBody = errors.New("actionmailer: no RenderBody seam configured")

	// ErrNoDeliveryMethod is returned when a message must be delivered but no
	// [DeliveryMethod] is configured.
	ErrNoDeliveryMethod = errors.New("actionmailer: no delivery method configured")
)

Sentinel errors returned during composition.

View Source
var ErrNoEval = errors.New("actionmailer: no EvalTemplate seam configured")

ErrNoEval is returned by a View when it must evaluate a template but no EvalTemplate seam is configured.

View Source
var GenerateBoundary = func(seq int) string { return fmt.Sprintf("boundary_%d", seq) }

GenerateBoundary produces a MIME multipart boundary for the seq-th container of a message. The default is deterministic (so composed output is reproducible in tests); a host may override it for random boundaries.

View Source
var GenerateContentID = func(name string) string { return "<" + name + ">" }

GenerateContentID produces the Content-ID header value for an inline attachment. The default derives it deterministically from the name; a host may override it (Rails uses a random token @ the host name).

Functions

This section is empty.

Types

type Action

type Action func(m *Mailer, params ...any) error

Action is a mailer action: the body of a Rails mailer method. It receives the per-invocation *Mailer (the mailer instance / `self`) and the caller's parameters, and is expected to call Mailer.Mail to compose the message.

type Attachment

type Attachment struct {
	// Name is the filename.
	Name string
	// Data is the decoded content (base64-encoded on the wire).
	Data []byte
	// ContentType is the media type; defaulted from the filename extension and
	// overridable before composition.
	ContentType string
	// Inline reports whether this is an inline (Content-Disposition: inline)
	// attachment carrying a Content-ID.
	Inline bool
	// ContentID is the Content-ID for an inline attachment (set on SetInline).
	ContentID string
}

Attachment is a single attachment, mirroring a Mail::Part built from `attachments[name] = data`.

type Attachments

type Attachments struct {
	// contains filtered or unexported fields
}

Attachments is the ordered attachment collection of a mailer action, mirroring the `attachments` proxy.

func (*Attachments) All

func (a *Attachments) All() []*Attachment

All returns every attachment in insertion order.

func (*Attachments) Get

func (a *Attachments) Get(name string) *Attachment

Get returns the attachment named name, or nil, mirroring `attachments[name]`.

func (*Attachments) Inline

func (a *Attachments) Inline() []*Attachment

Inline returns the inline attachments in insertion order.

func (*Attachments) Regular

func (a *Attachments) Regular() []*Attachment

Regular returns the non-inline attachments in insertion order.

func (*Attachments) Set

func (a *Attachments) Set(name string, data []byte) *Attachment

Set adds (or replaces) a regular attachment named name and returns it, mirroring `attachments[name] = data`.

func (*Attachments) SetInline

func (a *Attachments) SetInline(name string, data []byte) *Attachment

SetInline adds (or replaces) an inline attachment and returns it, assigning a Content-ID, mirroring `attachments.inline[name] = data`.

type Base

type Base struct {
	// Name is the mailer class name (e.g. "UserMailer"), passed to the
	// [RenderBody] seam so it can locate templates.
	Name string

	// Defaults holds the default headers/params (the Rails `default from: …`).
	// Recognised keys "from", "to", "cc", "bcc", "reply_to", "subject" and
	// "content_type" fill the corresponding message fields when an action omits
	// them; any other key becomes a default header.
	Defaults map[string]string

	// RenderBody is the body-rendering seam (nil unless bodies are rendered).
	RenderBody RenderBody

	// DeliveryMethod is the transport used by DeliverNow/DeliverLater.
	DeliveryMethod DeliveryMethod

	// PerformDeliveries gates whether the delivery method is actually invoked
	// (ActionMailer::Base.perform_deliveries). Defaults to true.
	PerformDeliveries bool

	// RaiseDeliveryErrors reports whether a delivery-method error propagates
	// (ActionMailer::Base.raise_delivery_errors). Defaults to true.
	RaiseDeliveryErrors bool

	// Deliveries is the sink used by [TestDelivery] (ActionMailer::Base.deliveries).
	Deliveries []*mail.Message

	// Now supplies the Date header when an action does not set one. When nil,
	// no Date is added. Defaults to time.Now.
	Now func() time.Time

	// MessageIDGen, when non-nil, supplies the Message-ID header.
	MessageIDGen func() string

	// EnqueueJob is the Active Job seam used by [MessageDelivery.DeliverLater].
	// When nil, DeliverLater runs the delivery inline.
	EnqueueJob func(job func() error) error

	// I18n resolves a subject when an action omits one (and no "subject" default
	// is set), mirroring ActionMailer's default_i18n_subject. When nil, an
	// action with no subject simply has no Subject header.
	I18n *I18n
	// contains filtered or unexported fields
}

Base is the analogue of a subclass of ActionMailer::Base: it holds the class-level configuration shared by every delivery, and the registry of named actions. Construct one with New.

func New

func New(name string) *Base

New creates a Base named name with Rails-default flags (perform_deliveries and raise_delivery_errors both true) and time.Now as the Date source.

func (*Base) Default

func (b *Base) Default(key, value string) *Base

Default sets a default param/header (Rails `default key: value`) and returns the receiver for chaining.

func (*Base) Process

func (b *Base) Process(action string, params ...any) *MessageDelivery

Process runs the named action to compose a message and returns a MessageDelivery, mirroring `MyMailer.action(params)`. Composition errors (unknown action, action error, render error, or an action that never called Mailer.Mail) are captured on the delivery and surface from its methods.

func (*Base) Register

func (b *Base) Register(action string, fn Action) *Base

Register binds an Action to a name so it can be run with Base.Process.

func (*Base) RegisterInterceptor

func (b *Base) RegisterInterceptor(i Interceptor) *Base

RegisterInterceptor adds a delivery interceptor.

func (*Base) RegisterObserver

func (b *Base) RegisterObserver(o Observer) *Base

RegisterObserver adds a delivery observer.

func (*Base) UseSendmail

func (b *Base) UseSendmail() *Base

UseSendmail wires the delivery method to a SendmailDelivery, mirroring `config.action_mailer.delivery_method = :sendmail`.

func (*Base) UseTestDelivery

func (b *Base) UseTestDelivery() *Base

UseTestDelivery wires the delivery method to a TestDelivery appending to b.Deliveries, mirroring `config.action_mailer.delivery_method = :test`.

func (*Base) UseView

func (b *Base) UseView(v *View) *Base

UseView installs v's View.RenderBody as the body-rendering seam, mirroring wiring Action View's template resolver to a mailer.

type DeliveryMethod

type DeliveryMethod interface {
	Deliver(m *mail.Message) error
}

DeliveryMethod is the pluggable transport that actually delivers a composed message, mirroring ActionMailer's delivery_method registry.

type EvalTemplate

type EvalTemplate func(source string, locals map[string]any) (string, error)

EvalTemplate is the template-evaluation seam: it compiles+runs a template source with the given locals and returns the rendered output. In Rails this is Action View handing the .erb to Erubis and running it; here a host wires it to go-ruby-erb + a Ruby runtime (rbgo), keeping this package CGO- and runtime-free. The View owns everything around it — template lookup, locale fallback, implicit text/html pairing, and layout wrapping.

type FileDelivery

type FileDelivery struct {
	// Location is the destination directory.
	Location string
	// contains filtered or unexported fields
}

FileDelivery is the :file delivery method: it writes one file per recipient into a directory, each containing the encoded message.

func NewFileDelivery

func NewFileDelivery(location string) *FileDelivery

NewFileDelivery returns a FileDelivery writing into location.

func (*FileDelivery) Deliver

func (f *FileDelivery) Deliver(m *mail.Message) error

Deliver writes the encoded message to Location/<recipient> for each To recipient.

type I18n

type I18n struct {
	// Locale is the active locale (default "en" when empty).
	Locale string
	// Translations maps a full dotted key to its (possibly interpolated) value.
	Translations map[string]string
}

I18n is the subject-translation store consulted when a mailer action calls mail(...) without a subject (and no "subject" default is set), mirroring ActionMailer's default_i18n_subject. Translations are keyed "<locale>.<mailer_scope>.<action>.subject" — for example "en.user_mailer.welcome.subject" — where the mailer scope is the mailer class name underscored. Values may embed %{name} interpolations resolved from the MailOptions.SubjectVars.

When no translation is found the humanised action name is used (the Rails default: "welcome" -> "Welcome").

func NewI18n

func NewI18n(locale string) *I18n

NewI18n returns an empty I18n store for locale (defaulting to "en").

func (*I18n) Set

func (i *I18n) Set(key, value string) *I18n

Set registers translation value for a dotted key (without the leading locale segment, which is prefixed automatically) and returns the store for chaining:

i18n.Set("user_mailer.welcome.subject", "Welcome, %{name}!")

type Interceptor

type Interceptor interface {
	DeliveringEmail(m *mail.Message)
}

Interceptor is registered with Base.RegisterInterceptor and is invoked with the fully composed message just before delivery, mirroring register_interceptor / Mail's inform_interceptors.

type MailOptions

type MailOptions struct {
	// From is the sender; when blank the "from" default is used.
	From string
	// To, Cc, Bcc and ReplyTo are recipient/reply lists; each list is joined
	// with ", " into its header. Blank lists fall back to the matching default.
	To, Cc, Bcc, ReplyTo []string
	// Subject is the subject; when blank the "subject" default is used, and if
	// that is blank too an i18n lookup (see [Base.I18n]) resolves it.
	Subject string
	// SubjectVars are the %{name} interpolation values for an i18n subject
	// lookup (the interpolations of ActionMailer's default_i18n_subject).
	SubjectVars map[string]any
	// Date sets the Date header when HasDate is true; otherwise Base.Now is used.
	Date    time.Time
	HasDate bool
	// Headers are extra headers set on the message (highest precedence).
	Headers map[string]string

	// Body, when non-empty, is used as a single-part body of ContentType
	// (default text/plain), bypassing the RenderBody seam.
	Body string
	// ContentType is the media type for an explicit Body (default text/plain).
	ContentType string

	// Formats lists the formats rendered via the RenderBody seam when Body is
	// empty (default {"text", "html"}).
	Formats []string
	// Locals are passed through to the RenderBody seam.
	Locals map[string]any
}

MailOptions are the keyword arguments of the Rails `mail(...)` call.

type Mailer

type Mailer struct {
	// contains filtered or unexported fields
}

Mailer is the per-invocation mailer instance — the `self` of a Rails mailer action. It accumulates attachments and extra headers and, via Mailer.Mail, composes the message. Actions receive it from Base.Process.

func (*Mailer) Attachments

func (m *Mailer) Attachments() *Attachments

Attachments returns the attachment collection for this action, mirroring the mailer's `attachments` accessor.

func (*Mailer) Headers

func (m *Mailer) Headers(h map[string]string) *Mailer

Headers merges the given headers into the message being built, mirroring the mailer's `headers(hash)` call. Later calls override earlier ones.

func (*Mailer) Mail

func (m *Mailer) Mail(opts MailOptions) error

Mail composes the message from opts and the accumulated attachments/headers, mirroring `mail(...)`. It stores the result so Base.Process can return it.

type MessageDelivery

type MessageDelivery struct {
	// contains filtered or unexported fields
}

MessageDelivery is the lazy delivery proxy returned by Base.Process, mirroring ActionMailer::MessageDelivery (`MyMailer.welcome(user)`). It carries the composed message, or the composition error, and exposes the deliver verbs.

func (*MessageDelivery) DeliverLater

func (d *MessageDelivery) DeliverLater() error

DeliverLater enqueues the delivery through the Active Job seam (Base.EnqueueJob), mirroring `.deliver_later`. When no seam is configured the delivery runs inline. Composition errors surface here.

func (*MessageDelivery) DeliverNow

func (d *MessageDelivery) DeliverNow() error

DeliverNow delivers the message immediately through the configured delivery method, mirroring `.deliver_now`. Composition errors surface here.

func (*MessageDelivery) Message

func (d *MessageDelivery) Message() (*mail.Message, error)

Message returns the composed message and any composition error, mirroring `.message`.

type Observer

type Observer interface {
	DeliveredEmail(m *mail.Message)
}

Observer is registered with Base.RegisterObserver and is invoked with the message just after delivery, mirroring register_observer / inform_observers.

type RenderBody

type RenderBody func(mailer, action, format string, locals map[string]any) (string, error)

RenderBody is the body-rendering seam. Given the mailer name, the action, a format ("text" / "html" / …) and the action's locals, it returns the rendered body for that format. Returning ErrNoTemplate signals that no template exists for the format, so it is skipped (mirroring Action View not finding a `.text` or `.html` template); any other error aborts composition.

type SMTPDelivery

type SMTPDelivery struct {
	// Addr is the "host:port" of the SMTP server.
	Addr string
	// Host is the server host used for PLAIN auth; when empty it is derived
	// from Addr.
	Host string
	// Username and Password enable PLAIN auth when Username is non-empty.
	Username string
	Password string
	// Send performs the send; defaults to net/smtp.SendMail.
	Send SMTPFunc
}

SMTPDelivery is the :smtp delivery method. The actual send is performed by Send (net/smtp.SendMail by default), which is injectable for testing.

func NewSMTPDelivery

func NewSMTPDelivery(addr string) *SMTPDelivery

NewSMTPDelivery returns an SMTPDelivery targeting addr ("host:port") using net/smtp.SendMail.

func (*SMTPDelivery) Deliver

func (s *SMTPDelivery) Deliver(m *mail.Message) error

Deliver sends the encoded message via SMTP to its To, Cc and Bcc recipients.

type SMTPFunc

type SMTPFunc func(addr string, a smtp.Auth, from string, to []string, msg []byte) error

SMTPFunc is the socket seam used by SMTPDelivery: it matches the signature of net/smtp.SendMail so tests can inject a fake and never open a socket.

type SendmailDelivery

type SendmailDelivery struct {
	// Location is the sendmail binary path (default "/usr/sbin/sendmail").
	Location string
	// Arguments are the flags passed to sendmail (default {"-i", "-t"}).
	Arguments []string
	// contains filtered or unexported fields
}

SendmailDelivery is the :sendmail delivery method: it pipes the encoded message to a local sendmail-compatible binary, mirroring ActionMailer's :sendmail. The default binary is /usr/sbin/sendmail invoked with "-i -t" (read recipients from the message, don't treat a lone dot as end-of-input).

func NewSendmailDelivery

func NewSendmailDelivery() *SendmailDelivery

NewSendmailDelivery returns a SendmailDelivery using /usr/sbin/sendmail -i -t.

func (*SendmailDelivery) Deliver

func (s *SendmailDelivery) Deliver(m *mail.Message) error

Deliver pipes the encoded message to the sendmail binary. When the arguments do not already carry -t, the To/Cc/Bcc recipients are appended so sendmail knows where to route the message (matching the gem, which appends the destinations when it is not reading them from the headers).

type TemplateStore

type TemplateStore struct {
	// contains filtered or unexported fields
}

TemplateStore holds mailer template, layout and partial sources, keyed for lookup with locale fallback — the in-memory analogue of Action View's resolver for the mail case. Register sources with TemplateStore.Add, TemplateStore.AddLocalized, TemplateStore.AddLayout and TemplateStore.AddPartial; lookups first try the requested locale, then the locale-less entry.

func NewTemplateStore

func NewTemplateStore() *TemplateStore

NewTemplateStore returns an empty TemplateStore.

func (*TemplateStore) Add

func (s *TemplateStore) Add(mailer, action, format, source string) *TemplateStore

Add registers a locale-less template for a mailer action + format (welcome.text.erb), returning the store for chaining.

func (*TemplateStore) AddLayout

func (s *TemplateStore) AddLayout(name, format, locale, source string) *TemplateStore

AddLayout registers a layout for a base name + format (layouts/mailer.html.erb), with an optional locale ("" for the fallback).

func (*TemplateStore) AddLocalized

func (s *TemplateStore) AddLocalized(mailer, action, format, locale, source string) *TemplateStore

AddLocalized registers a template for a specific locale (welcome.en.text.erb). A locale of "" is the fallback variant.

func (*TemplateStore) AddPartial

func (s *TemplateStore) AddPartial(name, format, locale, source string) *TemplateStore

AddPartial registers a partial by name + format (_footer.html.erb), with an optional locale ("" for the fallback).

type TestDelivery

type TestDelivery struct {
	// contains filtered or unexported fields
}

TestDelivery is the :test delivery method: it appends each delivered message to a slice (ActionMailer::Base.deliveries) instead of sending it.

func NewTestDelivery

func NewTestDelivery(dst *[]*mail.Message) *TestDelivery

NewTestDelivery returns a TestDelivery appending to dst.

func (*TestDelivery) Deliver

func (t *TestDelivery) Deliver(m *mail.Message) error

Deliver appends m to the target slice.

type View

type View struct {
	// Store holds the template, layout and partial sources.
	Store *TemplateStore
	// Eval evaluates a resolved template source. Required.
	Eval EvalTemplate
	// Locale is the active locale for lookups (default "en" when empty).
	Locale string
	// Layout is the default layout base name (e.g. "mailer"); "" renders bodies
	// without a layout.
	Layout string
	// Context is an optional go-ruby-actionview context supplying the view
	// helpers/routing available inside templates. It is copied per render so its
	// RenderTemplate seam can be wired to the store; a nil Context uses a zero one.
	Context *actionview.Context
}

View resolves and renders mailer templates and layouts (with locale fallback and implicit text/html pairing) through a go-ruby-actionview actionview.Context and an EvalTemplate seam, producing a RenderBody to wire to Base.RenderBody.

func (*View) RenderBody

func (v *View) RenderBody() RenderBody

RenderBody returns a RenderBody closure over the view: for each requested format it resolves the mailer's template (skipping the format with ErrNoTemplate when none exists), evaluates it through the actionview Context, and wraps the result in the format's layout (exposing the rendered body to the layout as the html-safe "yield" local) when a layout is configured.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL