Skip to content
latest
Framework Packages / latest

Email Package

github.com/mbvlabs/andurel/pkg/email defines transport-neutral messages, validates delivery contracts, classifies failures for retries, converts HTML to text, and ships development and SMTP transports. Templates and provider selection belong to the application (email/, config/ and internal/runtime).

Sender interfaces

Consumers depend on the capability they actually use:

1type TransactionalSender interface {
2    SendTransactional(ctx context.Context, payload TransactionalPayload) error
3}
4
5type MarketingSender interface {
6    SendMarketing(ctx context.Context, payload MarketingPayload) error
7}

A provider may implement one or both. EMAIL_PROVIDER selects the transport in config.NewMail, and internal/runtime publishes the interfaces, so services and workers never know which provider is active.

Messages and validation

TransactionalData and MarketingData are caller-facing values. The free functions validate them, copy the supported fields into a payload, and call the sender:

1err := email.SendTransactional(ctx, email.TransactionalData{
2    To:       user.Email,
3    From:     cfg.DefaultSenderSignature,
4    Subject:  "Verify your account",
5    HTMLBody: htmlBody,
6    TextBody: textBody,
7    Metadata: map[string]string{"user_id": user.ID.String()},
8}, sender)

Transactional mail needs at least one To, Cc or Bcc recipient, a sender, a subject, and an HTML body. Missing values return a ValidationError wrapping the matching sentinel (ErrMissingRecipient, ErrMissingSender, ErrMissingSubject, ErrMissingHTMLBody).

Marketing mail needs an unsubscribe URL and an HTML body. A missing unsubscribe URL returns ErrUnsubscribeURLRequired; IsValidationError recognizes that sentinel directly.

Attachment carries a name, bytes, content type, and an inline flag. Provider adapters may enforce additional limits.

Templates and text alternatives

Email is authored as templ under email/. A template type implements email.Transformer, and Compose renders both bodies in one call. Inside the email package, import the framework package under an alias so the names do not collide:

 1import pkgemail "github.com/mbvlabs/andurel/pkg/email"
 2
 3type WelcomeEmail struct {
 4    UserName string
 5}
 6
 7var _ pkgemail.Transformer = (*WelcomeEmail)(nil)
 8
 9func (w WelcomeEmail) ToHTML() (string, error) {
10    var buf bytes.Buffer
11    if err := w.render().Render(context.Background(), &buf); err != nil {
12        return "", err
13    }
14    return buf.String(), nil
15}
16
17func (w WelcomeEmail) ToText() (string, error) {
18    html, err := w.ToHTML()
19    if err != nil {
20        return "", err
21    }
22    return pkgemail.HTMLToText(html)
23}
1data, err := email.Compose(WelcomeEmail{UserName: user.Email}, to, from, "Welcome")

HTMLToText parses the rendered document, drops head, script and style, keeps useful link text and URLs, and normalizes whitespace. Treat it as a baseline. Author the text body explicitly when wording matters; the package does not inline CSS, rewrite assets or localize templates.

Run andurel sync email after the templates or the email CSS change: it compiles the Tailwind classes used by email/*.templ, inlines the resulting styles, and regenerates the *_templ.go files. Do not hand-edit the generated files.

Failure classification and retries

Error Meaning Retry?
ValidationError Caller input is invalid No
PermanentError The provider rejected the message No
TemporaryError A known transient failure Yes

IsValidationError recognizes a ValidationError and the validation sentinels. IsRetryable returns true for TemporaryError, false for PermanentError and validation failures, and true for an unclassified error so mail is not silently lost.

Adapters should translate provider failures into these types. Retry timeouts, throttling and transient service failures with bounded backoff; do not retry malformed payloads.

Transports

NewMailpit(MailpitConfig{Host, Port}) returns a local SMTP sender that implements both interfaces. Mailpit captures mail for inspection; it is refused in production by the generated configuration. It sends plaintext to the local relay and does not authenticate.

NewSMTP(SMTPConfig{Host, Port, Username, Password, TLS, Timeout}) sends through a production relay. TLS is one of:

  • email.SMTPTLSStartTLS (starttls, the default; use port 587);
  • email.SMTPTLSImplicit (tls; use port 465);
  • email.SMTPTLSNone (none; local relay only).

SMTPConfig.Validate requires a host and port, requires username and password together, rejects an unknown TLS mode, and refuses credentials without TLS unless the host is local. Timeout bounds dialing and I/O; the zero value means no explicit timeout.

Provider-only features belong behind an application interface. Keep provider adapters under internal/ and construct them in internal/runtime.

Queues and transactions

Email is an external side effect and cannot join a PostgreSQL transaction. For important mail, commit the domain state and a River job together, then render and send in an idempotent worker.

The framework supplies the job arguments and workers:

  • storage.SendTransactionalEmailArgs and storage.SendMarketingEmailArgs carry the message;
  • storage.SendTransactionalEmailWorker and storage.SendMarketingEmailWorker deliver it;
  • storage.AddEmailWorkers(workers, transactional, marketing) registers both;
  • storage.SendTransactional cancels the job when the failure is not retryable.

Enqueue from a service, passing the queue bound to the transaction:

1err := storage.AtomicOps(ctx, i.transactor, func(ctx context.Context, tx storage.Tx) error {
2    queue := i.queue.InTx(tx)
3    // … write domain state …
4    _, err := queue.Insert(ctx, storage.SendTransactionalEmailArgs{Data: data}, nil)
5    return err
6})

Job arguments persist in river_job. Do not put a verification code or reset link in the args: enqueue the user ID and let the worker mint the secret and render the message. Keep large attachments out of job arguments and record provider identifiers where delivery auditing matters. See Queues and Email.