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 ¶
- Variables
- func RenderNewRecommendationsEmail(data NotificationData) (string, error)
- func RenderPasswordResetEmail(email, resetURL string) (string, error)
- func RenderPasswordResetEmailHTML(email, resetURL string) (string, error)
- func RenderPurchaseApprovalRequestEmail(data NotificationData) (string, error)
- func RenderPurchaseApprovalRequestEmailHTML(data NotificationData) (string, error)
- func RenderPurchaseConfirmationEmail(data NotificationData) (string, error)
- func RenderPurchaseExecutedNotificationEmail(data NotificationData) (string, error)
- func RenderPurchaseExecutedNotificationEmailHTML(data NotificationData) (string, error)
- func RenderPurchaseFailedEmail(data NotificationData) (string, error)
- func RenderPurchaseScheduledDelayEmail(data NotificationData) (string, error)
- func RenderRIExchangeCompletedEmail(data RIExchangeNotificationData) (string, error)
- func RenderRIExchangePendingApprovalEmail(data RIExchangeNotificationData) (string, error)
- func RenderRIExchangePendingApprovalEmailHTML(data RIExchangeNotificationData) (string, error)
- func RenderRegistrationDecisionEmail(data RegistrationDecisionData) (string, error)
- func RenderRegistrationReceivedEmail(data RegistrationNotificationData) (string, error)
- func RenderScheduledPurchaseEmail(data NotificationData) (string, error)
- func RenderUserInviteEmail(email, setupURL string) (string, error)
- func RenderUserInviteEmailHTML(email, setupURL string) (string, error)
- func RenderWelcomeEmail(email, dashboardURL, role string) (string, error)
- func RenderWelcomeEmailHTML(email, dashboardURL, role string) (string, error)
- type FactoryConfig
- type MuteChecker
- type NopSender
- func (n *NopSender) SendNewRecommendationsNotification(_ context.Context, _ NotificationData) error
- func (n *NopSender) SendNotification(_ context.Context, _, _ string) error
- func (n *NopSender) SendPasswordResetEmail(_ context.Context, _, _ string) error
- func (n *NopSender) SendPurchaseApprovalRequest(_ context.Context, _ NotificationData) error
- func (n *NopSender) SendPurchaseConfirmation(_ context.Context, _ NotificationData) error
- func (n *NopSender) SendPurchaseExecutedNotification(_ context.Context, _ NotificationData) error
- func (n *NopSender) SendPurchaseFailedNotification(_ context.Context, _ NotificationData) error
- func (n *NopSender) SendPurchaseScheduledNotification(_ context.Context, _ NotificationData) error
- func (n *NopSender) SendRIExchangeCompleted(_ context.Context, _ RIExchangeNotificationData) error
- func (n *NopSender) SendRIExchangePendingApproval(_ context.Context, _ RIExchangeNotificationData) error
- func (n *NopSender) SendRegistrationDecisionNotification(_ context.Context, _ string, _ RegistrationDecisionData) error
- func (n *NopSender) SendRegistrationReceivedNotification(_ context.Context, _ RegistrationNotificationData) error
- func (n *NopSender) SendScheduledPurchaseNotification(_ context.Context, _ NotificationData) error
- func (n *NopSender) SendToEmail(_ context.Context, _, _, _ string) error
- func (n *NopSender) SendToEmailWithCCMultipart(_ context.Context, _ string, ccEmails []string, _, _, _ string) error
- func (n *NopSender) SendUserInviteEmail(_ context.Context, _, _ string) error
- func (n *NopSender) SendWelcomeEmail(_ context.Context, _, _, role string) error
- type NotificationData
- type PasswordResetData
- type ProviderType
- type RIExchangeItem
- type RIExchangeNotificationData
- type RecommendationSummary
- type RegistrationDecisionData
- type RegistrationNotificationData
- type SESEmailSender
- type SMTPConfig
- type SMTPSender
- func (s *SMTPSender) SendNewRecommendationsNotification(ctx context.Context, data NotificationData) error
- func (s *SMTPSender) SendNotification(ctx context.Context, subject, message string) error
- func (s *SMTPSender) SendPasswordResetEmail(ctx context.Context, email, resetURL string) error
- func (s *SMTPSender) SendPurchaseApprovalRequest(ctx context.Context, data NotificationData) error
- func (s *SMTPSender) SendPurchaseConfirmation(ctx context.Context, data NotificationData) error
- func (s *SMTPSender) SendPurchaseExecutedNotification(ctx context.Context, data NotificationData) error
- func (s *SMTPSender) SendPurchaseFailedNotification(ctx context.Context, data NotificationData) error
- func (s *SMTPSender) SendPurchaseScheduledNotification(ctx context.Context, data NotificationData) error
- func (s *SMTPSender) SendRIExchangeCompleted(ctx context.Context, data RIExchangeNotificationData) error
- func (s *SMTPSender) SendRIExchangePendingApproval(ctx context.Context, data RIExchangeNotificationData) error
- func (s *SMTPSender) SendRegistrationDecisionNotification(ctx context.Context, toEmail string, data RegistrationDecisionData) error
- func (s *SMTPSender) SendRegistrationReceivedNotification(ctx context.Context, data RegistrationNotificationData) error
- func (s *SMTPSender) SendScheduledPurchaseNotification(ctx context.Context, data NotificationData) error
- func (s *SMTPSender) SendToEmail(ctx context.Context, toEmail, subject, body string) error
- func (s *SMTPSender) SendToEmailWithCC(ctx context.Context, toEmail string, ccEmails []string, subject, body string) error
- func (s *SMTPSender) SendToEmailWithCCMultipart(ctx context.Context, toEmail string, ccEmails []string, ...) error
- func (s *SMTPSender) SendUserInviteEmail(ctx context.Context, email, setupURL string) error
- func (s *SMTPSender) SendWelcomeEmail(ctx context.Context, email, dashboardURL, role string) error
- func (s *SMTPSender) WithMuteChecker(mc MuteChecker) *SMTPSender
- func (s *SMTPSender) WithUnsubscribeBaseURL(u string) *SMTPSender
- type SNSPublisher
- type Sender
- func (s *Sender) SendNewRecommendationsNotification(ctx context.Context, data NotificationData) error
- func (s *Sender) SendNotification(ctx context.Context, subject, message string) error
- func (s *Sender) SendPasswordResetEmail(ctx context.Context, email, resetURL string) error
- func (s *Sender) SendPurchaseApprovalRequest(ctx context.Context, data NotificationData) error
- func (s *Sender) SendPurchaseConfirmation(ctx context.Context, data NotificationData) error
- func (s *Sender) SendPurchaseExecutedNotification(ctx context.Context, data NotificationData) error
- func (s *Sender) SendPurchaseFailedNotification(ctx context.Context, data NotificationData) error
- func (s *Sender) SendPurchaseScheduledNotification(ctx context.Context, data NotificationData) error
- func (s *Sender) SendRIExchangeCompleted(ctx context.Context, data RIExchangeNotificationData) error
- func (s *Sender) SendRIExchangePendingApproval(ctx context.Context, data RIExchangeNotificationData) error
- func (s *Sender) SendRegistrationDecisionNotification(ctx context.Context, toEmail string, data RegistrationDecisionData) error
- func (s *Sender) SendRegistrationReceivedNotification(ctx context.Context, data RegistrationNotificationData) error
- func (s *Sender) SendScheduledPurchaseNotification(ctx context.Context, data NotificationData) error
- func (s *Sender) SendToEmail(ctx context.Context, toEmail, subject, body string) error
- func (s *Sender) SendToEmailWithCC(ctx context.Context, toEmail string, ccEmails []string, subject, body string) error
- func (s *Sender) SendToEmailWithCCMultipart(ctx context.Context, toEmail string, ccEmails []string, ...) error
- func (s *Sender) SendUserInviteEmail(ctx context.Context, email, setupURL string) error
- func (s *Sender) SendWelcomeEmail(ctx context.Context, email, dashboardURL, role string) error
- func (s *Sender) WithMuteChecker(mc MuteChecker) *Sender
- func (s *Sender) WithUnsubscribeBaseURL(u string) *Sender
- type SenderConfig
- type SenderInterface
- type SkippedExchange
- type UserInviteData
- type WelcomeUserData
Constants ¶
This section is empty.
Variables ¶
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 ¶
RenderPasswordResetEmail renders the plain-text password reset email template.
func RenderPasswordResetEmailHTML ¶
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 ¶
RenderUserInviteEmail renders the plain-text user-invite email template.
func RenderUserInviteEmailHTML ¶
RenderUserInviteEmailHTML renders the HTML half of the user-invite email. Issue #355.
func RenderWelcomeEmail ¶
RenderWelcomeEmail renders the plain-text welcome email template.
func RenderWelcomeEmailHTML ¶
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 (*NopSender) SendPasswordResetEmail ¶
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 (*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 (*NopSender) SendToEmailWithCCMultipart ¶
func (*NopSender) SendUserInviteEmail ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
SkippedExchange represents an exchange that was skipped.
type UserInviteData ¶
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 ¶
WelcomeUserData holds data for welcome emails.