Skip to content

Email Providers

Email Providers are pluggable components that handle email delivery in Comby applications. They decouple your application logic from specific email services, allowing you to easily switch between different email delivery mechanisms.

Overview

The EmailProvider interface defines a standard contract for all email providers, supporting various delivery services:

  • SMTP - Standard SMTP protocol with TLS support
  • SendGrid - SendGrid email service
  • SES - Amazon Simple Email Service
  • Mailgun - Mailgun email service
  • Custom - Your own implementation
  • NoOp - No-operation provider for testing

The EmailProvider Interface

go
type EmailProvider interface {
    // SendEmail sends an email to one or more recipients
    SendEmail(ctx context.Context, params EmailParams) (*EmailDeliveryReceipt, error)

    // Type returns the provider type
    Type() EmailProviderType

    // String returns a string representation
    String() string
}

Available Implementations

SMTP Provider

Send emails using standard SMTP protocol with support for TLS encryption.

Installation

Built-in Go package, no additional dependencies required.

Configuration

go
provider := providers.NewSMTPEmailProvider(providers.SMTPConfig{
    Host:          "smtp.gmail.com",
    Port:          587,
    Username:      "your-email@gmail.com",
    Password:      "your-app-password",
    SenderEmail:   "noreply@example.com",
    SenderName:    "Example App",
    UseTLS:        false,            // false for STARTTLS (port 587)
    SkipTLSVerify: false,            // true to skip certificate verification (not recommended)
})

Common SMTP Configurations

Gmail:

go
SMTPConfig{
    Host:        "smtp.gmail.com",
    Port:        587,
    Username:    "your-email@gmail.com",
    Password:    "app-password",      // Generate at myaccount.google.com/apppasswords
    UseTLS:      false,                // STARTTLS
}

Office 365:

go
SMTPConfig{
    Host:        "smtp.office365.com",
    Port:        587,
    Username:    "your-email@domain.com",
    Password:    "your-password",
    UseTLS:      false,
}

Custom SMTP Server:

go
SMTPConfig{
    Host:        "mail.example.com",
    Port:        465,                  // Direct TLS
    Username:    "user@example.com",
    Password:    "password",
    UseTLS:      true,                 // Direct TLS connection
}

Template Customization

go
provider.WithTemplate(
    "default-invitation-reactor-invitation-created",
    "Invitation from {inviterInfo}",
    "Hi!\n\nYou've been invited by {inviterInfo}.\n\nAccept: https://myapp.com/invite/{token}",
)

SendGrid Provider

Send emails using SendGrid's API with advanced features and analytics.

Installation

go
import "github.com/sendgrid/sendgrid-go"

Configuration

go
provider := providers.NewSendgridEmailProvider(
    "SG.xxxxx",                    // SendGrid API key
    "noreply@example.com",         // Sender email (must be verified)
    "Example App",                 // Sender name
)

Template Customization

go
provider.WithTemplate(
    "default-invitation-reactor-invitation-created",
    "Invitation from {inviterInfo}",
    "Hi!\n\nYou've been invited by {inviterInfo}.\n\nAccept: https://myapp.com/invite/{token}",
)

Features

  • Privacy-friendly (tracking disabled by default)
  • Category tagging for analytics
  • Custom templates per action
  • Message ID tracking
  • High deliverability

Mailgun Provider

Send emails using Mailgun's API with EU/US region support.

Installation

Built-in HTTP client, no additional dependencies required.

Configuration

go
provider := providers.NewMailgunEmailProvider(providers.MailgunConfig{
    APIKey:      "your-api-key",
    Domain:      "mg.example.com",
    SenderEmail: "noreply@example.com",
    SenderName:  "Example App",
    UseEURegion: false,              // true for EU region
})

Template Customization

go
provider.WithTemplate(
    "default-invitation-reactor-invitation-created",
    "Invitation from {inviterInfo}",
    "Hi!\n\nYou've been invited by {inviterInfo}.\n\nAccept: https://myapp.com/invite/{token}",
)

Features

  • EU and US region support
  • Privacy-friendly (tracking disabled)
  • Tag-based organization
  • Message ID tracking
  • Powerful analytics

Template System

How Templates Work

  1. Action-Based: Templates are registered for specific actions
  2. Placeholder Replacement: {key} placeholders are replaced with actual values
  3. Fallback: If no template exists, uses subject/body from EmailParams
  4. Metadata Integration: All metadata values are available as placeholders

Template Structure

go
type EmailTemplate struct {
    Subject string  // Email subject with placeholders
    Body    string  // Email body with placeholders
}

Available Placeholders

Templates can use any key from EmailParams.Metadata as a placeholder:

go
provider.WithTemplate(
    "user-invitation",
    "{appName} - Invitation from {inviterName}",
    `Hello {recipientName}!

You have been invited to join {appName} by {inviterName}.

Click here to accept: {invitationLink}

This invitation will expire in {expiresIn} hours.

Best regards,
The {appName} Team`,
)

Special Placeholders

When using templates, these special placeholders are also available:

  • {subject} - Original subject from EmailParams
  • {body} - Original body from EmailParams

This allows you to create wrapper templates:

go
provider.WithTemplate(
    "notification",
    "[Notification] {subject}",
    `This is an automated notification from My App.

{body}

---
To unsubscribe, visit: https://myapp.com/unsubscribe`,
)

Sending Emails

Basic Email

go
receipt, err := provider.SendEmail(ctx, comby.EmailParams{
    Action:  "user-notification",
    To:      []string{"user@example.com"},
    Subject: "Welcome!",
    Body:    "Welcome to our application.",
    Metadata: map[string]string{
        "category": "onboarding",
    },
})

Email with Template

go
// 1. Register template
provider.WithTemplate(
    "user-invitation",
    "Invitation to {appName}",
    "Hi {name}! Join us at {invitationLink}",
)

// 2. Send email (template placeholders replaced automatically)
receipt, err := provider.SendEmail(ctx, comby.EmailParams{
    Action: "user-invitation",
    To:     []string{"user@example.com"},
    Metadata: map[string]string{
        "appName":        "My Application",
        "name":           "John Doe",
        "invitationLink": "https://myapp.com/invite/abc123",
    },
})

Note: When a template is registered for an action, the Subject and Body fields in EmailParams are ignored (unless used via {subject} or {body} placeholders).

Multiple Recipients

go
receipt, err := provider.SendEmail(ctx, comby.EmailParams{
    Action:  "team-announcement",
    To:      []string{
        "user1@example.com",
        "user2@example.com",
        "user3@example.com",
    },
    Subject: "Team Meeting Tomorrow",
    Body:    "Don't forget about our team meeting at 10 AM.",
})

With Metadata

go
receipt, err := provider.SendEmail(ctx, comby.EmailParams{
    Action:  "password-reset",
    To:      []string{"user@example.com"},
    Subject: "Password Reset Request",
    Body:    "Click here to reset: {resetLink}",
    Metadata: map[string]string{
        "resetLink": "https://myapp.com/reset/token123",
        "expiresIn": "24",
        "category":  "security",
        "priority":  "high",
    },
})

Common Actions

Actions are typically formatted as: domain-component-operation-details

Examples of common actions in Comby:

ActionDescription
default-invitation-reactor-invitation-createdUser invitation emails
default-account-reactor-password-resetPassword reset emails
default-account-reactor-email-verificationEmail verification
custom-notification-system-alertSystem alerts
custom-billing-invoice-createdInvoice notifications

Integration with Reactors

Email providers are commonly used in Reactors to send notifications based on events:

go
type NotificationReactor struct {
    emailProvider comby.EmailProvider
}

func (r *NotificationReactor) React(ctx context.Context, event comby.Event) error {
    // Extract event data
    userEmail := event.Payload["email"].(string)
    userName := event.Payload["name"].(string)

    // Send notification email
    _, err := r.emailProvider.SendEmail(ctx, comby.EmailParams{
        Action:  "user-registered",
        To:      []string{userEmail},
        Subject: "Welcome!",
        Body:    "Welcome message",
        Metadata: map[string]string{
            "userName": userName,
            "appName":  "My App",
        },
    })

    return err
}

Implementing Custom Email Providers

To implement your own email provider:

  1. Implement the Interface: Implement EmailProvider interface
  2. Choose Provider Type: Return appropriate EmailProviderType
  3. Support Templates: Allow users to customize messages
  4. Provide Configuration: Accept necessary credentials and settings
  5. Return Receipts: Provide delivery receipts with tracking information

Example Structure

go
type CustomEmailProvider struct {
    config    Config
    templates map[string]comby.EmailTemplate
}

func NewCustomEmailProvider(config Config) *CustomEmailProvider {
    return &CustomEmailProvider{
        config:    config,
        templates: make(map[string]comby.EmailTemplate),
    }
}

func (p *CustomEmailProvider) WithTemplate(action, subject, body string) *CustomEmailProvider {
    p.templates[action] = comby.EmailTemplate{
        Subject: subject,
        Body:    body,
    }
    return p
}

func (p *CustomEmailProvider) SendEmail(ctx context.Context, params comby.EmailParams) (*comby.EmailDeliveryReceipt, error) {
    // 1. Get template (if exists)
    var subject, body string
    if template, exists := p.templates[params.Action]; exists {
        subject = template.Subject
        body = template.Body
    } else {
        subject = params.Subject
        body = params.Body
    }

    // 2. Replace placeholders
    if params.Metadata != nil {
        for key, value := range params.Metadata {
            placeholder := fmt.Sprintf("{%s}", key)
            subject = strings.ReplaceAll(subject, placeholder, value)
            body = strings.ReplaceAll(body, placeholder, value)
        }
    }

    // 3. Send via your delivery mechanism
    messageID, err := p.sendViaCustomService(params.To, subject, body)
    if err != nil {
        return nil, err
    }

    // 4. Return receipt
    return &comby.EmailDeliveryReceipt{
        MessageID: messageID,
        Timestamp: time.Now().Unix(),
        Metadata: map[string]string{
            "recipients": strings.Join(params.To, ","),
            "action":     params.Action,
        },
    }, nil
}

func (p *CustomEmailProvider) Type() comby.EmailProviderType {
    return comby.EmailProviderTypeCustom
}

func (p *CustomEmailProvider) String() string {
    return "custom-email-provider"
}

Best Practices

Configuration

  1. Use Environment Variables: Store credentials in environment variables
  2. Verify Sender: Ensure sender email is verified with your provider
  3. Test Thoroughly: Test with real emails before production
  4. Monitor Limits: Be aware of sending limits and quotas
  5. Handle Failures: Implement retry logic for transient failures

Template Design

  1. Clear Subject Lines: Make subjects descriptive and concise
  2. Plain Text: Start with plain text, add HTML later if needed
  3. Mobile Friendly: Keep content readable on small screens
  4. Call to Action: Make it clear what the user should do
  5. Unsubscribe Link: Include unsubscribe options for marketing emails

Content Guidelines

go
// Good: Clear, actionable, and concise
provider.WithTemplate(
    "password-reset",
    "Reset Your Password",
    `Someone requested a password reset for your account.

Click here to reset: {resetLink}

This link expires in {expiresIn} hours.

If you didn't request this, ignore this email.`,
)

// Avoid: Vague, wordy, or confusing
provider.WithTemplate(
    "password-reset",
    "Important Information Regarding Your Account Security",
    `We have received a request to reset the password associated with your account...
[long paragraph]
To proceed with the password reset procedure, please click on the following URL...`,
)

Security

  1. Sensitive Data: Never include passwords or sensitive data in emails
  2. Token Expiration: Always set expiration on action links
  3. HTTPS Links: Use HTTPS for all links
  4. Rate Limiting: Implement throttling on email sending
  5. Validate Recipients: Verify email addresses before sending

Error Handling

go
receipt, err := provider.SendEmail(ctx, params)
if err != nil {
    // Log error with context
    logger.Error("failed to send email",
        "error", err,
        "action", params.Action,
        "recipients", len(params.To))

    // Return user-friendly error
    return fmt.Errorf("failed to send email notification")
}

// Log success
logger.Info("email sent successfully",
    "action", params.Action,
    "messageID", receipt.MessageID)

Testing

Use NoOp provider during testing:

go
// test_config.go
func NewTestEmailProvider() comby.EmailProvider {
    return &NoOpEmailProvider{}
}

// production_config.go
func NewProductionEmailProvider() comby.EmailProvider {
    return providers.NewSendgridEmailProvider(
        os.Getenv("SENDGRID_API_KEY"),
        os.Getenv("SENDER_EMAIL"),
        os.Getenv("SENDER_NAME"),
    )
}

Common Metadata Keys

KeyDescriptionExample
appNameApplication name"My Application"
userNameUser's name"John Doe"
inviterNameInviter's name"Jane Smith"
invitationLinkInvitation URL"https://app.com/invite/abc123"
resetLinkPassword reset URL"https://app.com/reset/token"
expiresInExpiration time"24" (hours)
categoryEmail category"security", "marketing"
priorityPriority level"high", "normal", "low"
templateIdExternal template ID"d-abc123" (SendGrid)

Provider Comparison

FeatureSMTPSendGridMailgunSES
Setup ComplexityLowLowLowMedium
CostFree (own server)Pay-as-you-goPay-as-you-goPay-as-you-go
DeliverabilityVariesHighHighHigh
AnalyticsNoneAdvancedAdvancedBasic
Template SupportYes (Comby)Yes (Comby + Native)Yes (Comby)Yes (Comby)
Region SupportAnyGlobalUS/EUAWS Regions
Rate LimitsServer-dependentPlan-dependentPlan-dependentSoft limits
Best ForSmall apps, testingHigh volume, marketingHigh volume, EU complianceAWS ecosystem

Troubleshooting

Email Not Delivered

SMTP:

  • Verify SMTP credentials are correct
  • Check firewall doesn't block SMTP ports
  • Ensure sender email matches authentication
  • Try different port (587 vs 465 vs 25)

SendGrid:

  • Verify API key is correct and not expired
  • Check sender email is verified in SendGrid
  • Review SendGrid activity feed for errors
  • Check domain authentication (SPF/DKIM)

Mailgun:

  • Verify API key and domain are correct
  • Check domain is active in Mailgun dashboard
  • Ensure recipient is not in suppression list
  • Review Mailgun logs for delivery status

Template Not Applied

go
// Ensure action matches exactly
provider.WithTemplate("user-invite", "Subject", "Body")

// Must use same action when sending
params := comby.EmailParams{
    Action: "user-invite",  // Must match exactly (case-sensitive)
    To:     []string{"user@example.com"},
}

Placeholder Not Replaced

go
// Template: "Hello {userName}"
// Metadata must include matching key:
Metadata: map[string]string{
    "userName": "John",  // NOT "username" or "user_name"
}

Authentication Failures

SMTP:

  • Gmail requires App Passwords (not account password)
  • Office 365 may require OAuth2 for some accounts
  • Check if 2FA is enabled and app passwords are needed

SendGrid/Mailgun:

  • Verify API key has not been revoked
  • Check API key has necessary permissions
  • Ensure API key is not restricted by IP