email

package
v0.0.0-...-ae331a0 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: OSL-3.0 Imports: 23 Imported by: 0

Documentation

Overview

Package email provides email notification functionality across multiple cloud providers.

Package email provides email notification functionality using SNS/SES.

Package email provides email notification functionality using SMTP.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoRecipient — the NotificationData has no RecipientEmail for a send
	// that requires a specific recipient (e.g. purchase approval for the user
	// who submitted the purchase, not a broadcast to SNS subscribers).
	ErrNoRecipient = errors.New("email: no recipient address")

	// ErrNoFromEmail — the sender has no FROM_EMAIL configured; nothing can go
	// out. Distinct from ErrNoRecipient so the caller can report which side
	// of the wire is unconfigured.
	ErrNoFromEmail = errors.New("email: no from address")

	// ErrTokenInBroadcast is returned by SendNotification when the message
	// body contains a "token=" query parameter. Broadcasting a body with an
	// approval token leaks it to every SNS subscriber. Use SendToEmailWithCC
	// (targeted SES) for any message that carries an approval URL.
	ErrTokenInBroadcast = errors.New("email: message body contains an approval token; use targeted SES send, not SNS broadcast")
)

Sentinel errors returned by send-path methods when preconditions aren't met. Callers use errors.Is to branch on these without pattern-matching on strings.

Functions

func RenderNewRecommendationsEmail

func RenderNewRecommendationsEmail(data NotificationData) (string, error)

RenderNewRecommendationsEmail renders the plain-text new recommendations email template.

func RenderPasswordResetEmail

func RenderPasswordResetEmail(email, resetURL string) (string, error)

RenderPasswordResetEmail renders the plain-text password reset email template.

func RenderPasswordResetEmailHTML

func RenderPasswordResetEmailHTML(email, resetURL string) (string, error)

RenderPasswordResetEmailHTML renders the HTML half of the password reset email — pair with RenderPasswordResetEmail for multipart/alternative delivery. Issue #355.

func RenderPurchaseApprovalRequestEmail

func RenderPurchaseApprovalRequestEmail(data NotificationData) (string, error)

RenderPurchaseApprovalRequestEmail renders the plain-text purchase approval request email template. Issue #287: this is the multipart text/plain half -- pair with RenderPurchaseApprovalRequestEmailHTML for the styled HTML half.

func RenderPurchaseApprovalRequestEmailHTML

func RenderPurchaseApprovalRequestEmailHTML(data NotificationData) (string, error)

RenderPurchaseApprovalRequestEmailHTML renders the HTML half of the purchase approval request email. Inline-styled per email-client constraints (Outlook etc. don't load external stylesheets reliably). The plain-text half (RenderPurchaseApprovalRequestEmail) carries the same content; receiving clients pick whichever they support via the multipart/alternative wrapper assembled by the sender.

func RenderPurchaseConfirmationEmail

func RenderPurchaseConfirmationEmail(data NotificationData) (string, error)

RenderPurchaseConfirmationEmail renders the plain-text purchase confirmation email template.

func RenderPurchaseExecutedNotificationEmail

func RenderPurchaseExecutedNotificationEmail(data NotificationData) (string, error)

RenderPurchaseExecutedNotificationEmail renders the plain-text half of the post-execution notification email (issue #291).

func RenderPurchaseExecutedNotificationEmailHTML

func RenderPurchaseExecutedNotificationEmailHTML(data NotificationData) (string, error)

RenderPurchaseExecutedNotificationEmailHTML renders the HTML half of the post-execution notification email. Pair with RenderPurchaseExecutedNotificationEmail for multipart/alternative delivery.

func RenderPurchaseFailedEmail

func RenderPurchaseFailedEmail(data NotificationData) (string, error)

RenderPurchaseFailedEmail renders the plain-text purchase failed email template.

func RenderPurchaseScheduledDelayEmail

func RenderPurchaseScheduledDelayEmail(data NotificationData) (string, error)

RenderPurchaseScheduledDelayEmail renders the plain-text scheduled-delay notification email.

func RenderRIExchangeCompletedEmail

func RenderRIExchangeCompletedEmail(data RIExchangeNotificationData) (string, error)

RenderRIExchangeCompletedEmail renders the plain-text RI exchange completed email template.

func RenderRIExchangePendingApprovalEmail

func RenderRIExchangePendingApprovalEmail(data RIExchangeNotificationData) (string, error)

RenderRIExchangePendingApprovalEmail renders the plain-text RI exchange pending approval email template. Pair with RenderRIExchangePendingApprovalEmailHTML for multipart/alternative delivery.

func RenderRIExchangePendingApprovalEmailHTML

func RenderRIExchangePendingApprovalEmailHTML(data RIExchangeNotificationData) (string, error)

RenderRIExchangePendingApprovalEmailHTML renders the HTML half of the RI exchange pending approval email -- inline-styled per email-client constraints. The plain-text half (RenderRIExchangePendingApprovalEmail) carries the same content; receiving clients pick whichever they support via the multipart/alternative wrapper assembled by the sender. Issue #296.

func RenderRegistrationDecisionEmail

func RenderRegistrationDecisionEmail(data RegistrationDecisionData) (string, error)

RenderRegistrationDecisionEmail renders the plain-text registrant notification for approval/rejection.

func RenderRegistrationReceivedEmail

func RenderRegistrationReceivedEmail(data RegistrationNotificationData) (string, error)

RenderRegistrationReceivedEmail renders the plain-text admin notification for a new registration.

func RenderScheduledPurchaseEmail

func RenderScheduledPurchaseEmail(data NotificationData) (string, error)

RenderScheduledPurchaseEmail renders the plain-text scheduled purchase email template.

func RenderUserInviteEmail

func RenderUserInviteEmail(email, setupURL string) (string, error)

RenderUserInviteEmail renders the plain-text user-invite email template.

func RenderUserInviteEmailHTML

func RenderUserInviteEmailHTML(email, setupURL string) (string, error)

RenderUserInviteEmailHTML renders the HTML half of the user-invite email. Issue #355.

func RenderWelcomeEmail

func RenderWelcomeEmail(email, dashboardURL, role string) (string, error)

RenderWelcomeEmail renders the plain-text welcome email template.

func RenderWelcomeEmailHTML

func RenderWelcomeEmailHTML(email, dashboardURL, role string) (string, error)

RenderWelcomeEmailHTML renders the HTML half of the welcome email. Issue #355.

Types

type FactoryConfig

type FactoryConfig struct {
	// Common configuration
	FromEmail string
	Provider  ProviderType

	// AWS-specific
	TopicARN     string
	EmailAddress string // Legacy: for SNS notifications

	// GCP-specific (SendGrid)
	SendGridAPIKey string

	// Azure-specific
	AzureSMTPUsername string
	AzureSMTPPassword string
	AzureSMTPHost     string // Defaults to "smtp.azurecomm.net" if empty
}

FactoryConfig holds configuration for creating email senders.

type MuteChecker

type MuteChecker interface {
	IsNotificationMuted(ctx context.Context, recipientEmail, scope string) (bool, error)
}

MuteChecker is a narrow interface the send path uses to consult the muted_recipients table. Isolating it from the full config.StoreInterface keeps the email package free of a direct dependency on the config package.

type NopSender

type NopSender struct{}

NopSender is a SenderInterface implementation that does not send anything. It logs every invocation at debug level so local-dev / EMAIL_ENABLED=false deployments can still trace where an email would have gone without requiring real SES / SNS / Azure / GCP credentials.

PII hygiene: logs only the method name and (for multi-recipient methods) the recipient counts. We deliberately avoid logging email addresses, subjects, or template-data payloads — even in dev, log files commonly leak into shared environments (terminal scrollback, screen-shares, support tickets), and addresses are sufficient identifying information to require treating them as PII.

func NewNopSender

func NewNopSender() *NopSender

NewNopSender constructs a no-op sender. Used when EMAIL_ENABLED=false.

func (*NopSender) SendNewRecommendationsNotification

func (n *NopSender) SendNewRecommendationsNotification(_ context.Context, _ NotificationData) error

func (*NopSender) SendNotification

func (n *NopSender) SendNotification(_ context.Context, _, _ string) error

func (*NopSender) SendPasswordResetEmail

func (n *NopSender) SendPasswordResetEmail(_ context.Context, _, _ string) error

func (*NopSender) SendPurchaseApprovalRequest

func (n *NopSender) SendPurchaseApprovalRequest(_ context.Context, _ NotificationData) error

func (*NopSender) SendPurchaseConfirmation

func (n *NopSender) SendPurchaseConfirmation(_ context.Context, _ NotificationData) error

func (*NopSender) SendPurchaseExecutedNotification

func (n *NopSender) SendPurchaseExecutedNotification(_ context.Context, _ NotificationData) error

func (*NopSender) SendPurchaseFailedNotification

func (n *NopSender) SendPurchaseFailedNotification(_ context.Context, _ NotificationData) error

func (*NopSender) SendPurchaseScheduledNotification

func (n *NopSender) SendPurchaseScheduledNotification(_ context.Context, _ NotificationData) error

func (*NopSender) SendRIExchangeCompleted

func (n *NopSender) SendRIExchangeCompleted(_ context.Context, _ RIExchangeNotificationData) error

func (*NopSender) SendRIExchangePendingApproval

func (n *NopSender) SendRIExchangePendingApproval(_ context.Context, _ RIExchangeNotificationData) error

func (*NopSender) SendRegistrationDecisionNotification

func (n *NopSender) SendRegistrationDecisionNotification(_ context.Context, _ string, _ RegistrationDecisionData) error

func (*NopSender) SendRegistrationReceivedNotification

func (n *NopSender) SendRegistrationReceivedNotification(_ context.Context, _ RegistrationNotificationData) error

func (*NopSender) SendScheduledPurchaseNotification

func (n *NopSender) SendScheduledPurchaseNotification(_ context.Context, _ NotificationData) error

func (*NopSender) SendToEmail

func (n *NopSender) SendToEmail(_ context.Context, _, _, _ string) error

func (*NopSender) SendToEmailWithCCMultipart

func (n *NopSender) SendToEmailWithCCMultipart(_ context.Context, _ string, ccEmails []string, _, _, _ string) error

func (*NopSender) SendUserInviteEmail

func (n *NopSender) SendUserInviteEmail(_ context.Context, _, _ string) error

func (*NopSender) SendWelcomeEmail

func (n *NopSender) SendWelcomeEmail(_ context.Context, _, _, role string) error

type NotificationData

type NotificationData struct {
	RequestedAt              string
	RequestedByName          string
	ExecutionID              string
	PlanID                   string
	RevokeURL                string
	RevocationWindowClosesAt string
	ArcheraEducationURL      string
	PurchaseDate             string
	PlanName                 string
	ApprovalToken            string
	CancellationWindowNote   string
	DashboardURL             string
	RecipientEmail           string
	RequestedByEmail         string
	CCEmails                 []string
	AuthorizedApprovers      []string
	Recommendations          []RecommendationSummary
	DaysUntilPurchase        int
	TotalUpfrontCost         float64
	TotalSavings             float64
	// RevocationToken is the one-time token embedded in the revocation link
	// of a post-execution notification email. When non-empty, the template
	// renders a "Revoke this purchase" CTA that hits
	// /api/purchases/revoke/{ExecutionID}?token=<RevocationToken>.
	// Empty silently omits the revocation panel so other email flows are
	// unaffected.
	RevocationToken string
	// ExecutedAt is the ISO-8601 / RFC-3339 timestamp the purchase was
	// executed at. Used in the post-execution notification body.
	// Empty omits the timestamp from the body.
	ExecutedAt string
	// ExecutedBy is the email of the user who triggered execution (approved
	// the purchase). Used in the post-execution notification body.
	// Empty omits the field.
	ExecutedBy string
}

NotificationData holds data for rendering email templates.

type PasswordResetData

type PasswordResetData struct {
	Email    string
	ResetURL string
}

PasswordResetData holds data for password reset emails.

type ProviderType

type ProviderType string

ProviderType represents the cloud provider for email services.

const (
	ProviderAWS   ProviderType = "aws"
	ProviderGCP   ProviderType = "gcp"
	ProviderAzure ProviderType = "azure"
)

type RIExchangeItem

type RIExchangeItem struct {
	RecordID           string
	ApprovalToken      string
	SourceRIID         string
	SourceInstanceType string
	TargetInstanceType string
	PaymentDue         string
	ExchangeID         string
	Error              string
	TargetCount        int
	UtilizationPct     float64
}

RIExchangeItem represents a single exchange in an email notification.

type RIExchangeNotificationData

type RIExchangeNotificationData struct {
	DashboardURL string
	Mode         string
	Exchanges    []RIExchangeItem
	Skipped      []SkippedExchange
	TotalPayment string
	// RecipientEmail is the primary (To) inbox for the approval-required flow.
	// Must be set when Exchanges contain ApprovalToken values; leave empty
	// only for completion/broadcast notifications that carry no tokens.
	// When non-empty, SendRIExchangePendingApproval routes through targeted
	// SES (not the SNS broadcast topic). Mirrors NotificationData.RecipientEmail.
	RecipientEmail string
	// CCEmails carries additional recipients informed of the pending exchanges
	// but not the authorized approvers. Deduplicated against RecipientEmail.
	CCEmails []string
	// RequestedByName is the human-readable display name of the user who
	// triggered the exchange run. Empty falls back to RequestedByEmail.
	RequestedByName string
	// RequestedByEmail is the requester's email address. Empty omits the
	// requested-by block from the approval email.
	RequestedByEmail string
	// RequestedAt is the ISO-8601 / RFC3339 timestamp the exchange was
	// submitted. Empty omits the timestamp from the summary.
	RequestedAt string
	// CancellationWindowNote is short text rendered below the approve/reject
	// buttons. Empty falls back to a generic 6-hour note.
	CancellationWindowNote string
}

RIExchangeNotificationData holds data for RI exchange email templates.

type RecommendationSummary

type RecommendationSummary struct {
	Service        string
	ResourceType   string
	Engine         string
	Region         string
	Payment        string
	AccountLabel   string
	Count          int
	MonthlySavings float64
	Term           int
	UpfrontCost    float64
}

RecommendationSummary is a simplified recommendation for email display.

type RegistrationDecisionData

type RegistrationDecisionData struct {
	AccountName     string
	Provider        string
	ExternalID      string
	Decision        string // "approved" or "rejected"
	RejectionReason string
}

RegistrationDecisionData is used to render the registrant notification when their registration is approved or rejected.

type RegistrationNotificationData

type RegistrationNotificationData struct {
	AccountName  string
	Provider     string
	ExternalID   string
	ContactEmail string
	DashboardURL string
	// RecipientEmail is the primary (To) inbox — the first admin email,
	// or the global notification email if no admin has an email
	// configured. Leave empty to fall back to the SNS broadcast path.
	RecipientEmail string
	// CCEmails carry the remaining admin emails plus the global
	// notification email, deduped against RecipientEmail.
	CCEmails []string
	// AdminApprovers is the full set of admin emails that can approve or
	// reject this registration — rendered verbatim in the message body
	// so CC'd recipients know the action isn't theirs to take. The
	// account's own ContactEmail is intentionally NOT on this list
	// because the submitter can't self-approve their own registration.
	AdminApprovers []string
}

RegistrationNotificationData is used to render the admin notification when a new account registers via the federation IaC.

type SESEmailSender

type SESEmailSender interface {
	SendEmail(ctx context.Context, params *sesv2.SendEmailInput, optFns ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error)
	GetAccount(ctx context.Context, params *sesv2.GetAccountInput, optFns ...func(*sesv2.Options)) (*sesv2.GetAccountOutput, error)
	GetEmailIdentity(ctx context.Context, params *sesv2.GetEmailIdentityInput, optFns ...func(*sesv2.Options)) (*sesv2.GetEmailIdentityOutput, error)
	CreateEmailIdentity(ctx context.Context, params *sesv2.CreateEmailIdentityInput, optFns ...func(*sesv2.Options)) (*sesv2.CreateEmailIdentityOutput, error)
}

SESEmailSender defines the interface for SES send email operations.

type SMTPConfig

type SMTPConfig struct {
	Host          string
	Username      string
	Password      string //nolint:gosec // G101: field holds a user-supplied runtime password, not a hardcoded credential
	FromEmail     string
	FromName      string
	NotifyEmail   string
	Port          int
	UseTLS        bool
	AllowInsecure bool
}

SMTPConfig holds configuration for SMTP email sender.

type SMTPSender

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

SMTPSender handles sending email via SMTP (works for SendGrid, Azure ACS, and others).

func NewSMTPSender

func NewSMTPSender(cfg SMTPConfig) (*SMTPSender, error)

NewSMTPSender creates a new SMTP email sender.

func (*SMTPSender) SendNewRecommendationsNotification

func (s *SMTPSender) SendNewRecommendationsNotification(ctx context.Context, data NotificationData) error

SendNewRecommendationsNotification sends a notification about new recommendations.

func (*SMTPSender) SendNotification

func (s *SMTPSender) SendNotification(ctx context.Context, subject, message string) error

SendNotification is a no-op for SMTP senders (07-N4). SMTP has no pub/sub equivalent of SNS: there is no topic to publish to and no subscriber list to fan out to. As a result, GCP (SendGrid) and Azure (ACS SMTP) deployments do not receive broadcast notifications (new-recs, scheduled-purchase reminders without a recipient email, etc.). Callers that need broadcast behavior on non-AWS deployments must wire their own fan-out or configure an SNS-compatible endpoint. Targeted approval emails (SendPurchaseApprovalRequest, SendScheduledPurchaseNotification) are unaffected because they use SendToEmailWithCC directly.

func (*SMTPSender) SendPasswordResetEmail

func (s *SMTPSender) SendPasswordResetEmail(ctx context.Context, email, resetURL string) error

SendPasswordResetEmail sends a password reset email as multipart/ alternative (text + styled HTML). Issue #355.

func (*SMTPSender) SendPurchaseApprovalRequest

func (s *SMTPSender) SendPurchaseApprovalRequest(ctx context.Context, data NotificationData) error

SendPurchaseApprovalRequest sends a purchase approval request email via SMTP. Prefers data.RecipientEmail (the submitter's notification email from app settings) over the static SMTP-configured s.notifyEmail so the approval token lands in the right inbox per submitter.

Mute check + List-Unsubscribe mirror the SES (*Sender) path: if the recipient has opted out of purchase_approvals the email is silently skipped, muted CC addresses are dropped, and an RFC 8058 List-Unsubscribe header pair is added when an unsubscribe base URL is configured.

func (*SMTPSender) SendPurchaseConfirmation

func (s *SMTPSender) SendPurchaseConfirmation(ctx context.Context, data NotificationData) error

SendPurchaseConfirmation sends a confirmation email after successful purchase.

func (*SMTPSender) SendPurchaseExecutedNotification

func (s *SMTPSender) SendPurchaseExecutedNotification(ctx context.Context, data NotificationData) error

SendPurchaseExecutedNotification sends the post-execution notification email via SMTP. Mirrors SendPurchaseApprovalRequest: prefers data.RecipientEmail over the static s.notifyEmail. Issue #291.

func (*SMTPSender) SendPurchaseFailedNotification

func (s *SMTPSender) SendPurchaseFailedNotification(ctx context.Context, data NotificationData) error

SendPurchaseFailedNotification sends a notification when a purchase fails.

func (*SMTPSender) SendPurchaseScheduledNotification

func (s *SMTPSender) SendPurchaseScheduledNotification(ctx context.Context, data NotificationData) error

SendPurchaseScheduledNotification sends the Gmail-style pre-fire delay notification email via SMTP. Mirrors the Sender implementation's behavior.

func (*SMTPSender) SendRIExchangeCompleted

func (s *SMTPSender) SendRIExchangeCompleted(ctx context.Context, data RIExchangeNotificationData) error

SendRIExchangeCompleted sends an RI exchange completion email via SMTP.

func (*SMTPSender) SendRIExchangePendingApproval

func (s *SMTPSender) SendRIExchangePendingApproval(ctx context.Context, data RIExchangeNotificationData) error

SendRIExchangePendingApproval sends an RI exchange approval email via SMTP as multipart/alternative (plain-text + styled HTML). The body carries live approval tokens, so it is sent only to the resolved recipient (the submitter's notification email, falling back to the static SMTP notify address) plus the deduplicated CC list; it never broadcasts. Returns ErrNoRecipient when neither address is configured. Issue #296.

func (*SMTPSender) SendRegistrationDecisionNotification

func (s *SMTPSender) SendRegistrationDecisionNotification(ctx context.Context, toEmail string, data RegistrationDecisionData) error

SendRegistrationDecisionNotification sends approval/rejection to the registrant via SMTP.

func (*SMTPSender) SendRegistrationReceivedNotification

func (s *SMTPSender) SendRegistrationReceivedNotification(ctx context.Context, data RegistrationNotificationData) error

SendRegistrationReceivedNotification sends an email to CUDly administrators for a new registration via SMTP. Prefers the caller-resolved data.RecipientEmail + CCEmails (admin emails + global notify) so the To / Cc semantics match the "authorized reviewers" block in the body; falls back to the legacy static s.notifyEmail when the caller didn't resolve recipients (e.g. no admin users configured yet).

func (*SMTPSender) SendScheduledPurchaseNotification

func (s *SMTPSender) SendScheduledPurchaseNotification(ctx context.Context, data NotificationData) error

SendScheduledPurchaseNotification sends a notification about scheduled purchase.

func (*SMTPSender) SendToEmail

func (s *SMTPSender) SendToEmail(ctx context.Context, toEmail, subject, body string) error

SendToEmail sends an email directly to a specific email address via SMTP.

func (*SMTPSender) SendToEmailWithCC

func (s *SMTPSender) SendToEmailWithCC(ctx context.Context, toEmail string, ccEmails []string, subject, body string) error

SendToEmailWithCC sends an email with To + optional Cc recipients via SMTP. The Cc header is included in the message envelope so recipients see one another, and the SMTP RCPT TO list carries every address so each inbox actually receives the message.

func (*SMTPSender) SendToEmailWithCCMultipart

func (s *SMTPSender) SendToEmailWithCCMultipart(ctx context.Context, toEmail string, ccEmails []string, subject, textBody, htmlBody string) error

SendToEmailWithCCMultipart sends a multipart/alternative message (plain- text + HTML) via SMTP. htmlBody == "" degrades to a single-part text send for backwards compatibility with callers that don't have an HTML body.

func (*SMTPSender) SendUserInviteEmail

func (s *SMTPSender) SendUserInviteEmail(ctx context.Context, email, setupURL string) error

SendUserInviteEmail sends an invite-with-setup-link email to a user created without a password, as multipart/alternative (text + styled HTML). Issue #355.

func (*SMTPSender) SendWelcomeEmail

func (s *SMTPSender) SendWelcomeEmail(ctx context.Context, email, dashboardURL, role string) error

SendWelcomeEmail sends a welcome email to new users as multipart/ alternative (text + styled HTML). Issue #355.

func (*SMTPSender) WithMuteChecker

func (s *SMTPSender) WithMuteChecker(mc MuteChecker) *SMTPSender

WithMuteChecker returns a shallow copy of s with the given MuteChecker wired in, mirroring (*Sender).WithMuteChecker so the SMTP transport applies the same per-recipient mute suppression as SES.

func (*SMTPSender) WithUnsubscribeBaseURL

func (s *SMTPSender) WithUnsubscribeBaseURL(u string) *SMTPSender

WithUnsubscribeBaseURL returns a shallow copy of s with the given base URL set, mirroring (*Sender).WithUnsubscribeBaseURL. When non-empty the SMTP approval send emits RFC 8058 List-Unsubscribe headers.

type SNSPublisher

type SNSPublisher interface {
	Publish(ctx context.Context, params *sns.PublishInput, optFns ...func(*sns.Options)) (*sns.PublishOutput, error)
}

SNSPublisher defines the interface for SNS publish operations.

type Sender

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

Sender handles sending email notifications.

func NewSender

func NewSender(cfg SenderConfig) (*Sender, error)

NewSender creates a new email sender with default context.

func NewSenderWithClients

func NewSenderWithClients(snsClient SNSPublisher, sesClient SESEmailSender, cfg SenderConfig) *Sender

NewSenderWithClients creates a new email sender with custom clients (for testing).

func NewSenderWithContext

func NewSenderWithContext(ctx context.Context, cfg SenderConfig) (*Sender, error)

NewSenderWithContext creates a new email sender with the provided context.

func (*Sender) SendNewRecommendationsNotification

func (s *Sender) SendNewRecommendationsNotification(ctx context.Context, data NotificationData) error

SendNewRecommendationsNotification sends an email about new recommendations.

func (*Sender) SendNotification

func (s *Sender) SendNotification(ctx context.Context, subject, message string) error

SendNotification sends a notification email via SNS.

Guard: messages whose body contains "token=" (case-insensitive) are rejected with ErrTokenInBroadcast. Approval tokens must never reach the SNS broadcast topic because every subscriber would receive a working action link. Use SendToEmailWithCC for any message that carries an approval URL.

Subject sanitization (07-M3): the subject is passed through sanitizeHeader (strips CR/LF to prevent SNS parameter injection) and truncated to 100 bytes (SNS limit). Subjects built from user-controlled fields such as PlanName can otherwise cause an InvalidParameter error at publish time.

func (*Sender) SendPasswordResetEmail

func (s *Sender) SendPasswordResetEmail(ctx context.Context, email, resetURL string) error

SendPasswordResetEmail sends a password reset email as multipart/ alternative (plain text + styled HTML with a CTA button). Issue #355.

func (*Sender) SendPurchaseApprovalRequest

func (s *Sender) SendPurchaseApprovalRequest(ctx context.Context, data NotificationData) error

SendPurchaseApprovalRequest sends an email asking the user to approve a direct purchase. Routes through SES SendEmail (not the SNS alerts topic) because the approval URL carries a one-time token scoped to the submitter — broadcasting that to every SNS subscriber would leak the authorisation. Returns ErrNoRecipient when data.RecipientEmail is empty and ErrNoFromEmail when FROM_EMAIL is unconfigured, so the caller can surface a precise reason in the API response instead of the prior silent no-op.

Mute check: if the recipient has opted out of purchase_approvals via the List-Unsubscribe link, the email is silently skipped and nil is returned. A List-Unsubscribe / List-Unsubscribe-Post header pair (RFC 8058) is added to the outbound SES message when an unsubscribe base URL is configured.

func (*Sender) SendPurchaseConfirmation

func (s *Sender) SendPurchaseConfirmation(ctx context.Context, data NotificationData) error

SendPurchaseConfirmation sends a confirmation after successful purchases.

func (*Sender) SendPurchaseExecutedNotification

func (s *Sender) SendPurchaseExecutedNotification(ctx context.Context, data NotificationData) error

SendPurchaseExecutedNotification sends the post-execution notification email to the configured recipients (global notification_email, per-account contact emails, and the requester). data.RecipientEmail must be set to the primary To address; data.CCEmails carries additional recipients. data.RevocationToken and data.RevocationWindowClosesAt control the revocation-link panel in the template. Issue #291.

func (*Sender) SendPurchaseFailedNotification

func (s *Sender) SendPurchaseFailedNotification(ctx context.Context, data NotificationData) error

SendPurchaseFailedNotification sends a notification when purchases fail.

func (*Sender) SendPurchaseScheduledNotification

func (s *Sender) SendPurchaseScheduledNotification(ctx context.Context, data NotificationData) error

SendPurchaseScheduledNotification sends the Gmail-style pre-fire delay notification email immediately after an approval with delay > 0. The email tells the user when the purchase will execute and includes a one-click revoke link. Route: direct To/CC (same as approval request) because the revoke link is scoped to the execution ID.

Returns ErrNoRecipient when data.RecipientEmail is empty. The body carries a live, execution-scoped revoke link, so it must never fall back to the broadcast SendNotification path (which would leak the action link to every alert subscriber and break the ownership/RBAC model around revocation). Mirrors SendScheduledPurchaseNotification and the SMTP sender, which both require a resolved recipient for this email.

func (*Sender) SendRIExchangeCompleted

func (s *Sender) SendRIExchangeCompleted(ctx context.Context, data RIExchangeNotificationData) error

SendRIExchangeCompleted sends a notification about completed RI exchanges.

func (*Sender) SendRIExchangePendingApproval

func (s *Sender) SendRIExchangePendingApproval(ctx context.Context, data RIExchangeNotificationData) error

SendRIExchangePendingApproval sends an email with RI exchange approval links as multipart/alternative (plain-text + styled HTML). The rendered body contains per-exchange approve/reject links that carry live tokens; any subscriber of the SNS topic who received this body could approve spend they were never authorized for. This method therefore routes through targeted SES (SendToEmailWithCCMultipart), mirroring the hardened path used by SendPurchaseApprovalRequest, and never falls back to the SNS broadcast topic.

Returns ErrNoRecipient when data.RecipientEmail is empty. Callers must resolve a recipient (e.g. the global notification email from GlobalConfig) before invoking this method. Issue #296.

func (*Sender) SendRegistrationDecisionNotification

func (s *Sender) SendRegistrationDecisionNotification(ctx context.Context, toEmail string, data RegistrationDecisionData) error

SendRegistrationDecisionNotification sends an email to the registrant when their registration is approved or rejected.

func (*Sender) SendRegistrationReceivedNotification

func (s *Sender) SendRegistrationReceivedNotification(ctx context.Context, data RegistrationNotificationData) error

SendRegistrationReceivedNotification sends an email notifying CUDly administrators that a new account registration has been submitted. When data.RecipientEmail is set (caller resolved admin + global-notify recipients) the send routes through the targeted SES path so To / Cc semantics match the approver/visibility distinction embedded in the body. When RecipientEmail is empty the send falls back to the legacy SNS broadcast path so deployments that never configured admin users still get notified.

func (*Sender) SendScheduledPurchaseNotification

func (s *Sender) SendScheduledPurchaseNotification(ctx context.Context, data NotificationData) error

SendScheduledPurchaseNotification sends a notification about an upcoming automated purchase. The rendered body embeds approve/pause/cancel links that carry a live token, so it must be delivered to a specific recipient via targeted SES, never broadcast through the SNS topic.

Returns ErrNoRecipient when data.RecipientEmail is empty so the caller can surface a precise reason rather than silently dropping the notification.

func (*Sender) SendToEmail

func (s *Sender) SendToEmail(ctx context.Context, toEmail, subject, body string) error

SendToEmail sends an email directly to a specific email address via SES If SES is in sandbox mode, it will automatically verify the recipient email if needed.

func (*Sender) SendToEmailWithCC

func (s *Sender) SendToEmailWithCC(ctx context.Context, toEmail string, ccEmails []string, subject, body string) error

SendToEmailWithCC sends an email with a primary To recipient plus optional Cc recipients. The To recipient is treated as the authorized actor for the message (verified in sandbox mode) and Cc recipients are informed of the action without carrying the "you must do something" burden. Duplicate entries across To/Cc are stripped so a single inbox is never addressed twice.

func (*Sender) SendToEmailWithCCMultipart

func (s *Sender) SendToEmailWithCCMultipart(ctx context.Context, toEmail string, ccEmails []string, subject, textBody, htmlBody string) error

SendToEmailWithCCMultipart is the multipart/alternative variant of SendToEmailWithCC: callers pass both a plain-text body and an HTML body, SES emits a multipart message, and the recipient's mail client picks whichever rendering it supports. Mirrors SendToEmailWithCC for the To/Cc dedupe + sandbox-recipient verification — the only delta is the email content shape. htmlBody == "" degrades to a single-part text send so callers that don't have an HTML body don't need a separate code path.

func (*Sender) SendUserInviteEmail

func (s *Sender) SendUserInviteEmail(ctx context.Context, email, setupURL string) error

SendUserInviteEmail sends an invitation that links to the password-setup page as multipart/alternative (plain text + styled HTML with a CTA button). Used when an admin creates a user without supplying a password. Issue #355.

func (*Sender) SendWelcomeEmail

func (s *Sender) SendWelcomeEmail(ctx context.Context, email, dashboardURL, role string) error

SendWelcomeEmail sends a welcome email to a new user as multipart/ alternative (plain text + styled HTML with a CTA button). Issue #355.

func (*Sender) WithMuteChecker

func (s *Sender) WithMuteChecker(mc MuteChecker) *Sender

WithMuteChecker returns a shallow copy of s with the given MuteChecker wired in. Callers that have a DB-backed store use this to enable per-recipient mute suppression on outbound SES sends.

func (*Sender) WithUnsubscribeBaseURL

func (s *Sender) WithUnsubscribeBaseURL(u string) *Sender

WithUnsubscribeBaseURL returns a shallow copy of s with the given base URL set. When non-empty the sender appends List-Unsubscribe / List-Unsubscribe-Post headers (RFC 8058) to outbound SES messages for applicable scopes.

type SenderConfig

type SenderConfig struct {
	TopicARN     string
	FromEmail    string
	EmailAddress string // Legacy: for SNS notifications
}

SenderConfig holds configuration for the email sender.

type SenderInterface

type SenderInterface interface {
	SendNotification(ctx context.Context, subject, message string) error
	SendToEmail(ctx context.Context, toEmail, subject, body string) error
	SendToEmailWithCCMultipart(ctx context.Context, toEmail string, ccEmails []string, subject, textBody, htmlBody string) error
	SendNewRecommendationsNotification(ctx context.Context, data NotificationData) error
	SendScheduledPurchaseNotification(ctx context.Context, data NotificationData) error
	SendPurchaseConfirmation(ctx context.Context, data NotificationData) error
	SendPurchaseFailedNotification(ctx context.Context, data NotificationData) error
	SendPasswordResetEmail(ctx context.Context, email, resetURL string) error
	SendWelcomeEmail(ctx context.Context, email, dashboardURL, role string) error
	SendUserInviteEmail(ctx context.Context, email, setupURL string) error
	SendRIExchangePendingApproval(ctx context.Context, data RIExchangeNotificationData) error
	SendRIExchangeCompleted(ctx context.Context, data RIExchangeNotificationData) error
	SendPurchaseApprovalRequest(ctx context.Context, data NotificationData) error
	// SendPurchaseScheduledNotification sends the "approved with delay" email
	// immediately after an approval when Gmail-style pre-fire delay is configured
	// (issue #291 wave-2). Notifies the user that the purchase will execute at
	// RevocationWindowClosesAt and includes a one-click revoke link.
	SendPurchaseScheduledNotification(ctx context.Context, data NotificationData) error
	// SendPurchaseExecutedNotification fires after a purchase executes
	// (regardless of whether it came from the approval-email path or the
	// direct-execute path). Recipients: global notification_email, per-account
	// contact emails, and the requester. The data must carry RevocationToken
	// and RevocationWindowClosesAt so the email embeds a one-click revoke link
	// valid for the AWS cancel window.
	SendPurchaseExecutedNotification(ctx context.Context, data NotificationData) error
	SendRegistrationReceivedNotification(ctx context.Context, data RegistrationNotificationData) error
	SendRegistrationDecisionNotification(ctx context.Context, toEmail string, data RegistrationDecisionData) error
}

SenderInterface defines the methods required for sending emails.

func NewSenderFromEnvironment

func NewSenderFromEnvironment(ctx context.Context) (SenderInterface, error)

NewSenderFromEnvironment creates an email sender based on environment variables. It auto-detects the cloud provider from SECRET_PROVIDER or CLOUD_PROVIDER env vars. When EMAIL_ENABLED parses as false (local dev / disabled deployments), returns a no-op sender that logs invocations instead of failing on missing cloud creds — this lets the rest of the application come up without a real SES/SNS/Azure/GCP setup. Unset / empty / unparseable values keep the default (email enabled) so existing deployments that don't set the var are unaffected; an unparseable value emits a warning so the misconfiguration is visible in logs.

func NewSenderWithConfig

func NewSenderWithConfig(ctx context.Context, cfg FactoryConfig) (SenderInterface, error)

NewSenderWithConfig creates an email sender with explicit configuration.

type SkippedExchange

type SkippedExchange struct {
	SourceRIID         string
	SourceInstanceType string
	Reason             string
}

SkippedExchange represents an exchange that was skipped.

type UserInviteData

type UserInviteData struct {
	Email    string
	SetupURL string
}

UserInviteData holds data for invite emails sent to users that an admin created without a password. The recipient sets their own password by following SetupURL.

type WelcomeUserData

type WelcomeUserData struct {
	Email        string
	DashboardURL string
	Role         string
}

WelcomeUserData holds data for welcome emails.

Jump to

Keyboard shortcuts

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