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 ¶
- Variables
- func BuildMIME(msg EmailMessage) (*mail.Msg, error)
- func BuildSignedMIME(ctx context.Context, msg EmailMessage, signer Signer) (*mail.Msg, error)
- func ClassifyReply(text string) error
- func DefaultEmailIntegration(ctx context.Context, s *ent.Scoped) (*ent.Integration, error)
- func DomainOf(email string) string
- func FirstNonEmpty(values ...string) string
- func FormatSender(addr, name string) string
- func HasVerifiedSendingDomain(ctx context.Context, s *ent.Scoped, fromEmail string) (bool, error)
- func RecordSendOutcome(ctx context.Context, provider Provider, status SendStatus)
- type Catalog
- func (c *Catalog) BuildEmail(provider Provider, config []byte, signer Signer) (EmailSender, error)
- func (c *Catalog) ChannelOf(provider Provider) (Channel, bool)
- func (c *Catalog) Descriptor(channel Channel, provider Provider) (ProviderDescriptor, bool)
- func (c *Catalog) Validate(channel Channel, provider Provider, config []byte) error
- type Channel
- type DefaultFromer
- type EmailMessage
- type EmailSender
- type Provider
- type ProviderDescriptor
- type Quota
- type QuotaReader
- type Receipt
- type Resolver
- type SendStatus
- type Signer
Constants ¶
This section is empty.
Variables ¶
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.
var ErrNoProvider = fmt.Errorf("no default provider configured")
ErrNoProvider is returned when a workspace has no enabled default provider for the requested channel.
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 ¶
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 ¶
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 ¶
DefaultEmailIntegration is the Workspace's default, enabled email Integration: the one Outbound send goes through. It returns ErrNoProvider when there is none.
func DomainOf ¶
domainOf returns the lower-cased domain of an email address, or "" when there is no single "@".
func FirstNonEmpty ¶
FirstNonEmpty returns the first non-empty string, or "" if all are empty.
func FormatSender ¶
FormatSender renders an email sender as "Name <addr>" when a display name is present, otherwise just the address.
func HasVerifiedSendingDomain ¶
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 ¶
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 ¶
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.
type Channel ¶
type Channel string
Channel identifies a delivery medium. Values mirror the ent Integration.channel enum.
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.
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 ¶
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 ¶
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 ¶
NewResolver wires the resolver.
func (*Resolver) EmailSender ¶
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.
Source Files
¶
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. |