Help us shape the future of Go! Take the 2026 Go Developer Survey

messaging

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

Package messaging is the channel-agnostic management layer for workspace sending providers. It defines the per-channel send contracts (EmailSender today; SmsSender later), a provider catalog that maps a (channel, provider) pair to its validation + construction logic, and a resolver that turns a workspace's stored integration into a ready-to-use sender.

The split is deliberate: storage, encryption, CRUD, default-selection and the catalog are generic across channels, while the send interface is channel specific. Adding a provider = register a descriptor + an impl. Adding a channel = a new send interface + descriptors, reusing all of the above.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrBusy: the provider says we are sending too fast (SES Throttling, an SMTP
	// "too many messages" / "rate limit" 4xx reply). Back off briefly.
	ErrBusy = errors.New("provider is busy: sending too fast")
	// ErrQuotaExceeded: the provider's daily (24 hour) sending quota is spent. Capacity
	// returns gradually, so the backoff is long.
	ErrQuotaExceeded = errors.New("provider daily sending quota exceeded")
)

Provider replies that say the provider is busy, not that the message is bad (ADR 0023). An adapter wraps one of these around the provider's own error; the Outbound send turns it into a Deferral instead of a retryable failure.

View Source
var ErrNoProvider = errors.New("no default provider configured")

ErrNoProvider is returned when a workspace has no enabled default provider for the requested channel.

View Source
var ErrUnverifiedSendingDomain = errors.New("sender domain is not a verified sending domain")

ErrUnverifiedSendingDomain is returned by the send path when a workspace message's From domain is not a verified sending domain (ADR 0010 slice 3): a verified domain is required to send. Callers translate it — RFC 7807 on the sync transactional API, a failed recipient/run on the async broadcast and automation surfaces.

Functions

func BuildMIME

func BuildMIME(msg EmailMessage) (*mail.Msg, error)

BuildMIME turns an EmailMessage into a go-mail *mail.Msg. It is the single MIME builder shared by every email provider (smtp sends it directly; ses serializes it to raw bytes for SendRawEmail), so message structure — multipart/alternative, headers, encoding — is identical across providers.

From/FromName must already be resolved (msg-level value or provider default) by the caller. When both HTML and Text are present the message is multipart/alternative with text/plain first and text/html as the richer alternative (clients render the last part they understand). A single body is sent as-is.

func BuildSignedMIME

func BuildSignedMIME(ctx context.Context, msg EmailMessage, signer Signer) (*mail.Msg, error)

BuildSignedMIME builds the message via BuildMIME and attaches a native DKIM signer for msg.From's verified sending domain (ADR 0010). go-mail signs during WriteTo, so the same Msg signs identically whether the provider writes it directly (SES raw bytes) or through the SMTP DATA stream.

The gate (slice 3): when signer is non-nil (a workspace send) and From's domain is not a verified sending domain, the send is rejected with ErrUnverifiedSendingDomain — a verified domain is required to send. A nil signer (platform/system mail) is exempt and always sends unsigned.

func ClassifyReply

func ClassifyReply(text string) error

ClassifyReply tells whether a provider reply is a "too fast" or "quota exceeded" one. text is the provider's message. It returns ErrQuotaExceeded, ErrBusy, or nil when the reply says nothing of the kind. Providers differ only in how they phrase it, so the wording heuristics live in one place; each adapter decides whether the reply is a transient one before asking.

func DefaultEmailIntegration

func DefaultEmailIntegration(ctx context.Context, s *ent.Scoped) (*ent.Integration, error)

DefaultEmailIntegration is the Workspace's default, enabled email Integration: the one Outbound send goes through. It returns ErrNoProvider when there is none.

func DomainOf

func DomainOf(email string) string

DomainOf returns the lower-cased domain of an email address, or "" when there is no single "@".

func FormatSender

func FormatSender(addr, name string) string

FormatSender renders an email sender as "Name <addr>" when a display name is present, otherwise just the address.

func HasVerifiedSendingDomain

func HasVerifiedSendingDomain(ctx context.Context, s *ent.Scoped, fromEmail string) (bool, error)

HasVerifiedSendingDomain reports whether fromEmail's domain is a verified sending domain in the workspace. It is the plan-time counterpart to the send-time gate in BuildSignedMIME, letting callers (e.g. broadcast planning) reject up front instead of failing every recipient. An empty/malformed fromEmail reports false.

func RecordSendOutcome

func RecordSendOutcome(ctx context.Context, provider Provider, status SendStatus)

RecordSendOutcome counts one send outcome for a provider on the global meter provider (a no-op when telemetry is disabled).

Types

type Catalog

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

Catalog holds the registered provider descriptors keyed by (channel, provider).

func NewCatalog

func NewCatalog(descriptors ...ProviderDescriptor) *Catalog

NewCatalog builds a catalog from the given descriptors.

func (*Catalog) BuildEmail

func (c *Catalog) BuildEmail(provider Provider, config []byte, signer Signer) (EmailSender, error)

BuildEmail builds an EmailSender for an email-channel provider. signer (may be nil) is passed to the provider for native DKIM signing.

func (*Catalog) ChannelOf

func (c *Catalog) ChannelOf(provider Provider) (Channel, bool)

ChannelOf reports which channel a provider belongs to; ok is false for an unregistered provider.

func (*Catalog) Descriptor

func (c *Catalog) Descriptor(channel Channel, provider Provider) (ProviderDescriptor, bool)

Descriptor looks up a registered provider; ok is false for unknown pairs.

func (*Catalog) Validate

func (c *Catalog) Validate(channel Channel, provider Provider, config []byte) error

Validate runs the provider's semantic validation, or reports an unknown provider.

type Channel

type Channel string

Channel identifies a delivery medium. Values mirror the ent Integration.channel enum.

const (
	ChannelEmail Channel = "email"
	// ChannelSMS is reserved; no SMS provider is implemented yet.
	ChannelSMS Channel = "sms"
)

type DefaultFromer

type DefaultFromer interface {
	DefaultFrom() (addr, name string)
}

DefaultFromer is implemented by senders that fall back to the integration's configured From when a message carries none. Outbound send reads it to know the effective From domain before sending, so the verified-domain gate and the Sending-domain stamp use the address that will really go on the wire.

type EmailMessage

type EmailMessage struct {
	From     string
	FromName string
	To       string
	Subject  string
	HTML     string
	Text     string
	// ListUnsubscribeURL, when set, emits the RFC 8058 one-click unsubscribe
	// headers (List-Unsubscribe + List-Unsubscribe-Post). Set only on marketing
	// surfaces (Broadcast, Automation); never on Transactional (ADR 0012).
	ListUnsubscribeURL string
}

EmailMessage is the channel-specific payload for email sends. HTML is used when set, otherwise Text (the provider libraries take a single body).

type EmailSender

type EmailSender interface {
	Send(ctx context.Context, msg EmailMessage) (Receipt, error)
}

EmailSender is implemented by every email provider (smtp, ses, …).

type Provider

type Provider string

Provider identifies a concrete provider implementation. Values mirror the ent Integration.provider enum.

const (
	ProviderSMTP Provider = "smtp"
	ProviderSES  Provider = "ses"
)

type ProviderDescriptor

type ProviderDescriptor struct {
	Channel  Channel
	Provider Provider

	// Validate runs semantic checks on a decrypted, structurally-valid config
	// blob (the typed API union already enforces shape and required fields).
	Validate func(config []byte) error

	// Build constructs the live sender from a decrypted config blob. The signer
	// (may be nil) is stored by the sender to DKIM-sign outbound mail per ADR 0010.
	Build func(config []byte, signer Signer) (any, error)
}

ProviderDescriptor is the extensibility seam: it teaches the catalog how to validate and build one provider. Build returns a channel-specific sender as `any`; callers type-assert to the channel interface (e.g. EmailSender).

type Quota

type Quota struct {
	PerSecond *int
	PerDay    *int
}

Quota is what a provider reports about its own sending limits (ADR 0023). A nil field is a window the provider does not bound.

type QuotaReader

type QuotaReader interface {
	SendQuota(ctx context.Context) (Quota, error)
}

QuotaReader is implemented by a sender whose provider can report its quota (SES GetSendQuota). Providers that cannot (SMTP) do not implement it and have no provider ceiling.

func AsQuotaReader

func AsQuotaReader(sender EmailSender) (QuotaReader, bool)

AsQuotaReader reports the QuotaReader behind sender, looking through the metrics decorator Catalog.BuildEmail wraps every sender in (a type assertion on the decorator alone would never see the provider's own interface).

type Receipt

type Receipt struct {
	// MessageID is the provider's id for the accepted message (SES MessageId, or the
	// MIME Message-ID for SMTP). Recorded on the Outbound message for correlation.
	MessageID string
}

Receipt is what a provider returns for a message it accepted.

type Resolver

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

Resolver turns a workspace's stored integration into a live sender. It is the only in-scope consumer of the catalog's Build path; the marketing send engine that actually dispatches campaigns is a separate, future component.

func NewResolver

func NewResolver(cipher *secrets.Cipher, catalog *Catalog) *Resolver

NewResolver wires the resolver.

func (*Resolver) EmailSender

func (r *Resolver) EmailSender(ctx context.Context, s *ent.Scoped) (EmailSender, error)

EmailSender resolves the workspace's default, enabled email provider, decrypts its config and builds a ready sender. Returns ErrNoProvider when none exists.

type SendStatus

type SendStatus string

SendStatus is the outcome label of the email.send.outcomes counter. The set is closed: the counter carries only provider and status, never a tenant or recipient identifier (ADR 0018).

const (
	// SendAccepted: the provider accepted the message.
	SendAccepted SendStatus = "accepted"
	// SendError: the provider call failed.
	SendError SendStatus = "error"
	// SendBusy: the provider said we are sending too fast; the message is deferred
	// and retried, not failed (ADR 0023). Kept out of the error ratio.
	SendBusy SendStatus = "busy"
	// SendQuotaExceeded: the provider's daily quota is spent; the message is deferred
	// (ADR 0023). Kept out of the error ratio.
	SendQuotaExceeded SendStatus = "quota_exceeded"
	// SendBounce: the provider reported a bounce for a message it had accepted.
	SendBounce SendStatus = "bounce"
	// SendComplaint: the provider reported a spam complaint.
	SendComplaint SendStatus = "complaint"
)

type Signer

type Signer interface {
	// DKIMSigner returns a go-mail DKIM signer for mail sent from fromEmail, or
	// (nil, nil) when fromEmail's domain has no verified sending domain in scope
	// (slice 2 signs when possible; the send gate for unverified From is slice 3).
	DKIMSigner(ctx context.Context, fromEmail string) (*mail.DKIMSigner, error)
}

Signer resolves the native DKIM signer for an outbound message (ADR 0010). Signing is sphericon-native and transport-independent: the same signer applies identically across providers because both serialize the message via go-mail's WriteTo, which signs when a DKIM signer is set.

func NewDKIMSigner

func NewDKIMSigner(s *ent.Scoped, cipher *secrets.Cipher) Signer

NewDKIMSigner builds a Signer for the Workspace the scoped client is confined to.

Directories

Path Synopsis
Package registry assembles the default provider catalog.
Package registry assembles the default provider catalog.
Package ses implements the Amazon SES email provider.
Package ses implements the Amazon SES email provider.
Package smtp implements the SMTP email provider on top of wneessen/go-mail.
Package smtp implements the SMTP email provider on top of wneessen/go-mail.

Jump to

Keyboard shortcuts

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