Files
mailx/aws/aws.go
T

138 lines
3.8 KiB
Go
Raw Normal View History

2026-08-15 01:38:05 +08:00
// Package aws 提供 Amazon SES 邮件发送通道。
2024-11-20 19:42:07 +08:00
package aws
import (
"context"
2026-08-15 01:38:05 +08:00
"fmt"
"sync"
"time"
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
mailx "code.yun.ink/pkg/mailx"
2024-11-20 19:42:07 +08:00
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
2026-08-15 01:38:05 +08:00
"github.com/aws/aws-sdk-go/service/ses/sesiface"
2024-11-20 19:42:07 +08:00
)
2026-08-15 01:38:05 +08:00
// defaultTimeout 单次 API 调用的默认超时
const defaultTimeout = 30 * time.Second
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
// Config AWS SES 通道配置
type Config struct {
AccessKeyID string // AccessKey ID
AccessKeySecret string // AccessKey Secret
Region string // Region,默认 ap-northeast-1
Sender string // 默认发件人(必填,AWS 需要预先验证发件地址)
Timeout time.Duration // 单次 API 调用的超时,默认 30s;0 表示交给 ctx 控制
}
// Aws AWS SES 发送通道
2024-11-20 19:42:07 +08:00
type Aws struct {
2026-08-15 01:38:05 +08:00
cfg Config
initOnce sync.Once
svc sesiface.SESAPI
initErr error
2024-11-20 19:42:07 +08:00
}
2026-08-15 01:38:05 +08:00
// New 创建 AWS 通道
func New(cfg Config) *Aws {
if cfg.Region == "" {
cfg.Region = "ap-northeast-1"
}
return &Aws{cfg: cfg}
2025-08-10 21:17:10 +08:00
}
2026-08-15 01:38:05 +08:00
// Name 返回通道名称
func (a *Aws) Name() string { return "aws" }
2025-08-10 21:17:10 +08:00
2026-08-15 01:38:05 +08:00
// Send 发送一封邮件
func (a *Aws) Send(ctx context.Context, msg *mailx.Message) error {
logger := mailx.LoggerFromContext(ctx)
sender := msg.From
if sender == "" {
sender = a.cfg.Sender
}
if sender == "" {
return fmt.Errorf("%w: aws sender is required", mailx.ErrInvalidConfig)
}
if len(msg.Inline) > 0 {
return fmt.Errorf("%w: aws SendEmail does not support inline images; use raw message mode", mailx.ErrInvalidConfig)
2024-11-20 19:42:07 +08:00
}
2026-08-15 01:38:05 +08:00
svc, err := a.client()
if err != nil {
return err
2025-08-10 21:17:10 +08:00
}
2026-08-15 01:38:05 +08:00
input := &ses.SendEmailInput{
Source: aws.String(sender),
2024-11-20 19:42:07 +08:00
Destination: &ses.Destination{
2026-08-15 01:38:05 +08:00
ToAddresses: aws.StringSlice(msg.To),
CcAddresses: aws.StringSlice(msg.Cc),
BccAddresses: aws.StringSlice(msg.Bcc),
2024-11-20 19:42:07 +08:00
},
Message: &ses.Message{
2026-08-15 01:38:05 +08:00
Subject: &ses.Content{Data: aws.String(msg.Subject), Charset: aws.String("UTF-8")},
Body: awsBody(msg),
2024-11-20 19:42:07 +08:00
},
2026-08-15 01:38:05 +08:00
}
if msg.ReplyTo != "" {
input.ReplyToAddresses = aws.StringSlice([]string{msg.ReplyTo})
}
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
ctx, cancel := a.withTimeout(ctx)
defer cancel()
if _, err := svc.SendEmailWithContext(ctx, input); err != nil {
logger.Errorf(ctx, "mailx/aws: send to %v failed: %v", msg.To, err)
return fmt.Errorf("%w: %v", mailx.ErrSendFailed, err)
}
logger.Infof(ctx, "mailx/aws: sent to %v subject=%q", msg.To, msg.Subject)
return nil
}
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
// client 惰性初始化并复用 SES 客户端(线程安全)。
// 若已通过测试或其他方式注入 svc,则直接返回注入的客户端。
func (a *Aws) client() (sesiface.SESAPI, error) {
if a.svc != nil {
return a.svc, a.initErr
}
a.initOnce.Do(func() {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(a.cfg.Region),
Credentials: credentials.NewStaticCredentials(a.cfg.AccessKeyID, a.cfg.AccessKeySecret, ""),
})
if err != nil {
a.initErr = fmt.Errorf("%w: create aws session: %v", mailx.ErrInvalidConfig, err)
return
}
a.svc = ses.New(sess)
})
return a.svc, a.initErr
}
// withTimeout 叠加配置超时到 ctx(已有更早 deadline 时保持不变)
func (a *Aws) withTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
if a.cfg.Timeout <= 0 {
return ctx, func() {}
}
if dl, ok := ctx.Deadline(); ok && time.Until(dl) <= a.cfg.Timeout {
return ctx, func() {}
}
return context.WithTimeout(ctx, a.cfg.Timeout)
}
// awsBody 构造 SES Body:优先使用 Html,其次 Text;两者都有时同时提供
func awsBody(msg *mailx.Message) *ses.Body {
body := &ses.Body{}
if msg.Body != "" {
body.Html = &ses.Content{Data: aws.String(msg.Body), Charset: aws.String("UTF-8")}
}
if msg.TextBody != "" {
body.Text = &ses.Content{Data: aws.String(msg.TextBody), Charset: aws.String("UTF-8")}
}
return body
2024-11-20 19:42:07 +08:00
}