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
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
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:
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:
SMTPConfig{
Host: "smtp.office365.com",
Port: 587,
Username: "your-email@domain.com",
Password: "your-password",
UseTLS: false,
}Custom SMTP Server:
SMTPConfig{
Host: "mail.example.com",
Port: 465, // Direct TLS
Username: "user@example.com",
Password: "password",
UseTLS: true, // Direct TLS connection
}Template Customization
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
import "github.com/sendgrid/sendgrid-go"Configuration
provider := providers.NewSendgridEmailProvider(
"SG.xxxxx", // SendGrid API key
"noreply@example.com", // Sender email (must be verified)
"Example App", // Sender name
)Template Customization
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
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
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
- Action-Based: Templates are registered for specific actions
- Placeholder Replacement:
{key}placeholders are replaced with actual values - Fallback: If no template exists, uses subject/body from
EmailParams - Metadata Integration: All metadata values are available as placeholders
Template Structure
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:
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:
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
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
// 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
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
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:
| Action | Description |
|---|---|
default-invitation-reactor-invitation-created | User invitation emails |
default-account-reactor-password-reset | Password reset emails |
default-account-reactor-email-verification | Email verification |
custom-notification-system-alert | System alerts |
custom-billing-invoice-created | Invoice notifications |
Integration with Reactors
Email providers are commonly used in Reactors to send notifications based on events:
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:
- Implement the Interface: Implement
EmailProviderinterface - Choose Provider Type: Return appropriate
EmailProviderType - Support Templates: Allow users to customize messages
- Provide Configuration: Accept necessary credentials and settings
- Return Receipts: Provide delivery receipts with tracking information
Example Structure
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
- Use Environment Variables: Store credentials in environment variables
- Verify Sender: Ensure sender email is verified with your provider
- Test Thoroughly: Test with real emails before production
- Monitor Limits: Be aware of sending limits and quotas
- Handle Failures: Implement retry logic for transient failures
Template Design
- Clear Subject Lines: Make subjects descriptive and concise
- Plain Text: Start with plain text, add HTML later if needed
- Mobile Friendly: Keep content readable on small screens
- Call to Action: Make it clear what the user should do
- Unsubscribe Link: Include unsubscribe options for marketing emails
Content Guidelines
// 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
- Sensitive Data: Never include passwords or sensitive data in emails
- Token Expiration: Always set expiration on action links
- HTTPS Links: Use HTTPS for all links
- Rate Limiting: Implement throttling on email sending
- Validate Recipients: Verify email addresses before sending
Error Handling
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:
// 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
| Key | Description | Example |
|---|---|---|
appName | Application name | "My Application" |
userName | User's name | "John Doe" |
inviterName | Inviter's name | "Jane Smith" |
invitationLink | Invitation URL | "https://app.com/invite/abc123" |
resetLink | Password reset URL | "https://app.com/reset/token" |
expiresIn | Expiration time | "24" (hours) |
category | Email category | "security", "marketing" |
priority | Priority level | "high", "normal", "low" |
templateId | External template ID | "d-abc123" (SendGrid) |
Provider Comparison
| Feature | SMTP | SendGrid | Mailgun | SES |
|---|---|---|---|---|
| Setup Complexity | Low | Low | Low | Medium |
| Cost | Free (own server) | Pay-as-you-go | Pay-as-you-go | Pay-as-you-go |
| Deliverability | Varies | High | High | High |
| Analytics | None | Advanced | Advanced | Basic |
| Template Support | Yes (Comby) | Yes (Comby + Native) | Yes (Comby) | Yes (Comby) |
| Region Support | Any | Global | US/EU | AWS Regions |
| Rate Limits | Server-dependent | Plan-dependent | Plan-dependent | Soft limits |
| Best For | Small apps, testing | High volume, marketing | High volume, EU compliance | AWS 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
// 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
// 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
Related Documentation
- MFA Providers - Multi-factor authentication providers
- Reactors - Event-driven side effects
- Default Invitation System - Built-in invitation system
- Account System - User account management