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 DeliversOutside(sender EmailSender) bool
- func DomainOf(email string) string
- func EmailIntegration(ctx context.Context, s *ent.Scoped, id *int64) (*ent.Integration, error)
- func FormatSender(addr, name string) string
- func HasVerifiedSendingDomain(ctx context.Context, s *ent.Scoped, fromEmail string) (bool, error)
- func IntegrationKept(ctx context.Context, s *ent.Scoped, id int64) (bool, error)
- func RecordSendOutcome(ctx context.Context, provider Provider, status SendStatus)
- type Captured
- type Capturer
- type Catalog
- func (c *Catalog) BuildEmail(provider Provider, config []byte, deps Deps) (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 Deps
- type EmailMessage
- type EmailSender
- type Header
- type Origin
- type OutsideDeliverer
- type Provider
- type ProviderDescriptor
- type Quota
- type QuotaReader
- type Receipt
- type Resolver
- type ResolverOption
- type SendStatus
- type Signer
- type Surface
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 = errors.New("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 DeliversOutside ¶
func DeliversOutside(sender EmailSender) bool
DeliversOutside reports whether sender's provider delivers outside, looking through the metrics decorator Catalog.BuildEmail wraps every sender in. A sender that says nothing delivers outside.
func DomainOf ¶
DomainOf returns the lower-cased domain of an email address, or "" when there is no single "@".
func EmailIntegration ¶
EmailIntegration is the enabled email Integration a send goes through: the one named by id, or the Workspace's default when id is nil. A named Integration that is disabled or discarded is ErrNoProvider, never the default in its place.
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 IntegrationKept ¶
IntegrationKept reports whether the Workspace has a kept Integration with the id: new work may name it. An archived one leaves new work (ADR 0049), though work already naming it keeps it as history, and one of another Workspace is not there. It says nothing of whether the Integration can send: a send checks that itself (EmailIntegration).
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 Captured ¶
type Captured struct {
Message EmailMessage
MessageID string
Headers []Header
Links []string
}
Captured is a message as a provider that delivers nowhere outside keeps it: the message, the Message-ID it gave it, the header lines it would carry on the wire in order, and the URLs of the links of its HTML part.
type Capturer ¶
Capturer keeps a message a provider accepted instead of sending it out. It is bound to one Workspace and one Integration.
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 from deps.
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 Deps ¶
type Deps struct {
// Signer DKIM-signs outbound mail per ADR 0010; may be nil.
Signer Signer
// Capturer keeps the messages of a provider that delivers nowhere outside (the
// Test inbox); may be nil, and a provider that delivers outside ignores it.
Capturer Capturer
}
Deps is what the catalog hands every provider builder, bound to one Workspace's scoped handle (zero fields when a sender is built outside a send, e.g. for a quota lookup).
type EmailMessage ¶
type EmailMessage struct {
From string
FromName string
To string
Subject string
HTML string
Text string
// ReplyTo, when set, is the address a reply goes to instead of From. A provider fills it
// from its configured default when the message carries none.
ReplyTo 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
// Origin says which send the message belongs to. A provider that delivers outside ignores
// it; the Test inbox records it on the Captured message. Nil for a message with no send behind it.
Origin *Origin
}
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 Origin ¶
Origin is where a message came from: the Outbound message of its send (zero for a Test send, which records none), the surface that asked for it and, for a marketing send, its Sending source. A message with no Origin is a Test send.
type OutsideDeliverer ¶
type OutsideDeliverer interface {
DeliversOutside() bool
}
OutsideDeliverer is implemented by a sender that says whether its provider delivers outside sphericon. A sender that does not implement it delivers outside (SMTP, SES). Outbound send reads it through DeliversOutside, never by the provider's name: the Sending-domain gate and DKIM signing apply only to a sender that delivers outside.
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 and the
// dependencies bound to the Workspace it sends for. A provider uses what it needs
// and ignores the rest.
Build func(config []byte, deps Deps) (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
// DeliveryConfirmed is set by a provider that confirmed delivery when it accepted the
// message (a Test inbox keeps it, so nothing later reports it). Recording the send then
// stamps the delivery result. SMTP and SES never set it: their Delivery notices arrive
// later from the provider.
DeliveryConfirmed bool
}
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, opts ...ResolverOption) *Resolver
NewResolver wires the resolver.
func (*Resolver) EmailSender ¶
func (r *Resolver) EmailSender(_ context.Context, s *ent.Scoped, row *ent.Integration) (EmailSender, error)
EmailSender decrypts the config of row, an Integration the caller resolved with EmailIntegration, and builds a ready sender. Returns ErrNoProvider when row is nil.
type ResolverOption ¶
type ResolverOption func(*Resolver)
ResolverOption configures a Resolver.
func WithCapturers ¶
func WithCapturers(bind func(s *ent.Scoped, integrationID int64) Capturer) ResolverOption
WithCapturers binds the Capturer a provider builder receives to the Workspace's scoped handle and to the Integration the sender is built for.
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.
type Surface ¶
type Surface string
Surface is the part of the product that asked for a send. Its values are those of the Outbound message kinds (broadcast, automation, transactional), plus SurfaceTest.
const SurfaceTest Surface = "test"
SurfaceTest is a Test send of a Broadcast or a template, which has no Outbound message.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package inbox implements the Test inbox provider (ADR 0058): it delivers nothing outside and keeps each message it is sent as a Captured message through the Capturer the catalog hands its builder.
|
Package inbox implements the Test inbox provider (ADR 0058): it delivers nothing outside and keeps each message it is sent as a Captured message through the Capturer the catalog hands its builder. |
|
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. |