yao/messenger/providers/mailgun/mailgun.go

219 lines
4.9 KiB
Go

package mailgun
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/yaoapp/yao/messenger/types"
)
// Provider implements the Provider interface for Mailgun email sending
type Provider struct {
config types.ProviderConfig
domain string
apiKey string
from string
baseURL string
httpClient *http.Client
}
// NewMailgunProvider creates a new Mailgun provider
func NewMailgunProvider(config types.ProviderConfig) (*Provider, error) {
provider := &Provider{
config: config,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
// Extract options
options := config.Options
if options == nil {
return nil, fmt.Errorf("Mailgun provider requires options")
}
// Required options
if domain, ok := options["domain"].(string); ok {
provider.domain = domain
} else {
return nil, fmt.Errorf("Mailgun provider requires 'domain' option")
}
if apiKey, ok := options["api_key"].(string); ok {
provider.apiKey = apiKey
} else {
return nil, fmt.Errorf("Mailgun provider requires 'api_key' option")
}
if from, ok := options["from"].(string); ok {
provider.from = from
} else {
return nil, fmt.Errorf("Mailgun provider requires 'from' option")
}
// Optional options
if baseURL, ok := options["base_url"].(string); ok {
provider.baseURL = baseURL
} else {
// Default to US region
provider.baseURL = "https://api.mailgun.net/v3"
}
return provider, nil
}
// Send sends a message using Mailgun
func (p *Provider) Send(ctx context.Context, message *types.Message) error {
if message.Type != types.MessageTypeEmail {
return fmt.Errorf("Mailgun provider only supports email messages")
}
return p.sendEmail(ctx, message)
}
// SendBatch sends multiple messages in batch
func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) error {
for _, message := range messages {
if err := p.Send(ctx, message); err != nil {
return fmt.Errorf("failed to send message to %v: %w", message.To, err)
}
}
return nil
}
// GetType returns the provider type
func (p *Provider) GetType() string {
return "mailgun"
}
// GetName returns the provider name
func (p *Provider) GetName() string {
return p.config.Name
}
// GetPublicInfo returns public information about the provider
func (p *Provider) GetPublicInfo() types.ProviderPublicInfo {
description := "Mailgun email service provider"
if p.config.Description != "" {
description = p.config.Description
}
return types.ProviderPublicInfo{
Name: p.config.Name,
Type: "mailgun",
Description: description,
Capabilities: []string{"email", "webhooks", "tracking"},
Features: types.Features{
SupportsWebhooks: true,
SupportsReceiving: false,
SupportsTracking: true,
SupportsScheduling: true,
},
}
}
// Validate validates the provider configuration
func (p *Provider) Validate() error {
if p.domain == "" {
return fmt.Errorf("domain is required")
}
if p.apiKey == "" {
return fmt.Errorf("api_key is required")
}
if p.from == "" {
return fmt.Errorf("from address is required")
}
return nil
}
// Close closes the provider connection (no-op for HTTP-based Mailgun)
func (p *Provider) Close() error {
return nil
}
// sendEmail sends an email via Mailgun API
func (p *Provider) sendEmail(ctx context.Context, message *types.Message) error {
apiURL := fmt.Sprintf("%s/%s/messages", p.baseURL, p.domain)
// Prepare form data
data := url.Values{}
// From address
from := message.From
if from == "" {
from = p.from
}
data.Set("from", from)
// To addresses
for _, to := range message.To {
data.Add("to", to)
}
// Subject and content
data.Set("subject", message.Subject)
if message.Body != "" {
data.Set("text", message.Body)
}
if message.HTML != "" {
data.Set("html", message.HTML)
}
// Custom headers
if message.Headers != nil {
for key, value := range message.Headers {
data.Set("h:"+key, value)
}
}
// Custom variables (metadata)
if message.Metadata != nil {
for key, value := range message.Metadata {
if str, ok := value.(string); ok {
data.Set("v:"+key, str)
}
}
}
// Priority
if message.Priority > 0 {
data.Set("o:priority", fmt.Sprintf("%d", message.Priority))
}
// Scheduled sending
if message.ScheduledAt != nil {
data.Set("o:deliverytime", message.ScheduledAt.Format(time.RFC1123Z))
}
// Create request with context
req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(data.Encode()))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Set authentication
req.SetBasicAuth("api", p.apiKey)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// Send request
resp, err := p.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Check response
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("Mailgun API error: %s - %s", resp.Status, string(body))
}
return nil
}