package aliyun import ( "context" "errors" "fmt" "strings" "time" "code.yun.ink/pkg/mailx/interfaces" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" dm20151123 "github.com/alibabacloud-go/dm-20151123/v2/client" util "github.com/alibabacloud-go/tea-utils/v2/service" "github.com/alibabacloud-go/tea/tea" ) const ( MaxRecipients = 100 MaxRetries = 3 DefaultEndpoint = "dm.aliyuncs.com" ) type Aliyun struct { interfaces.DefaultEmail client *dm20151123.Client // params *interfaces.EmialConfigDataAliyun // logger loggerx.LoggerInterface } func NewAliyun() *Aliyun { aliyun := &Aliyun{} aliyun.Options = interfaces.DefaultOptions() aliyun.EmailType = interfaces.EmailTypeAliyun return aliyun } func (l *Aliyun) SetOption(ctx context.Context, opt ...interfaces.Option) error { for _, o := range opt { o(&l.Options) } if l.Options.Aliyun == nil { return fmt.Errorf("Aliyun configuration is required") } // 验证配置 if err := l.validateConfig(); err != nil { return fmt.Errorf("invalid Aliyun config: %w", err) } // 安全日志输出 l.Options.Logger.Infof(ctx, "Aliyun configured - Endpoint:%s AccountName:%s", l.Options.Aliyun.Endpoint, l.Options.Aliyun.AccountName) config := &openapi.Config{ AccessKeyId: tea.String(l.Options.Aliyun.AccessId), AccessKeySecret: tea.String(l.Options.Aliyun.AccessKey), } if l.Options.Aliyun.Endpoint == "" { l.Options.Aliyun.Endpoint = DefaultEndpoint } config.Endpoint = tea.String(l.Options.Aliyun.Endpoint) client, err := dm20151123.NewClient(config) if err != nil { return fmt.Errorf("failed to create Aliyun client: %w", err) } l.IsSet = true l.client = client return nil } func (l *Aliyun) Send(ctx context.Context, params interfaces.Message) error { if !l.IsSet { return fmt.Errorf("Aliyun client not initialized") } // 验证消息 if err := l.validateMessage(params); err != nil { return fmt.Errorf("invalid message: %w", err) } // 重试机制 var lastErr error for i := 0; i < MaxRetries; i++ { if i > 0 { l.Options.Logger.Infof(ctx, "Retrying Aliyun email send, attempt %d/%d", i+1, MaxRetries) time.Sleep(time.Duration(i) * time.Second) } lastErr = l.sendEmail(ctx, params) if lastErr == nil { l.Options.Logger.Infof(ctx, "Aliyun email sent successfully to %v", params.To) return nil } l.Options.Logger.Errorf(ctx, "Aliyun send attempt %d failed: %v", i+1, lastErr) } return fmt.Errorf("failed to send Aliyun email after %d attempts: %w", MaxRetries, lastErr) } // 同步状态 func (l *Aliyun) SyncStatus(ctx context.Context) (resp []interfaces.EmailSendRecord, err error) { start := "" // 一次同步一天的数据 for { list, next, err := l.getSendStatus(ctx, start) l.Options.Logger.Infof(ctx, "list:%+v next:%+v err:%+v", list, next, err) if err != nil { return nil, err } for _, val := range list { t, _ := time.ParseInLocation("2006-01-02T15:04Z", tea.StringValue(val.LastUpdateTime), time.Local) // 0:成功 2:无效地址 3:垃圾邮件 4:失败 record := interfaces.EmailSendRecord{ AccountName: tea.StringValue(val.AccountName), UpdateTime: t.UnixMilli(), ToUser: tea.StringValue(val.ToAddress), Subject: tea.StringValue(val.Subject), ErrorMessage: tea.StringValue(val.Message), } switch tea.Int32Value(val.Status) { case 0: record.Status = interfaces.EmailSendStatusSuccess case 2: record.Status = interfaces.EmailSendStatusInvalidAddress case 3: record.Status = interfaces.EmailSendStatusSpam case 4: record.Status = interfaces.EmailSendStatusFailed default: record.Status = interfaces.EmailSendStatusUnknown } resp = append(resp, record) } if next == nil || len(*next) == 0 { break } start = *next } return resp, nil } func (l *Aliyun) getSendStatus(ctx context.Context, start string) (list []*dm20151123.SenderStatisticsDetailByParamResponseBodyDataMailDetail, nextStart *string, err error) { now := time.Now().Local() senderStatisticsDetailByParamRequest := &dm20151123.SenderStatisticsDetailByParamRequest{ StartTime: tea.String(now.AddDate(0, 0, -1).Format("2006-01-02 15:04")), EndTime: tea.String(now.Format("2006-01-02 15:04")), Length: tea.Int32(100), } if start != "" { senderStatisticsDetailByParamRequest.NextStart = tea.String(start) } runtime := &util.RuntimeOptions{} tryErr := func() (_e error) { defer func() { if r := tea.Recover(recover()); r != nil { _e = r } }() // 复制代码运行请自行打印 API 的返回值 resp, _err := l.client.SenderStatisticsDetailByParamWithOptions(senderStatisticsDetailByParamRequest, runtime) if _err != nil { l.Options.Logger.Errorf(ctx, "resp:%+v err:%+v", resp, _err) return _err } if resp == nil || resp.Body == nil || resp.Body.Data == nil { return errors.New("resp.Body.Data is nil") } list = resp.Body.Data.MailDetail nextStart = resp.Body.NextStart return nil }() if tryErr != nil { l.Options.Logger.Errorf(ctx, "err:%+v", tryErr) return nil, nil, tryErr // var error = &tea.SDKError{} // if _t, ok := tryErr.(*tea.SDKError); ok { // error = _t // } else { // error.Message = tea.String(tryErr.Error()) // } // // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 // // 错误 message // fmt.Println(tea.StringValue(error.Message)) // // 诊断地址 // var data interface{} // d := json.NewDecoder(strings.NewReader(tea.StringValue(error.Data))) // d.Decode(&data) // if m, ok := data.(map[string]interface{}); ok { // recommend, _ := m["Recommend"] // fmt.Println("recommend:", recommend) // } // _, _err := util.AssertAsString(error.Message) // if _err != nil { // l.logger.Errorf(ctx, "resp:%+v err:%+v", error.Message, _err) // return nil, nil, _err // } } return list, nextStart, nil } // 验证配置 func (l *Aliyun) validateConfig() error { if l.Options.Aliyun.AccessId == "" { return errors.New("AccessId is required") } if l.Options.Aliyun.AccessKey == "" { return errors.New("AccessKey is required") } if l.Options.Aliyun.AccountName == "" { return errors.New("AccountName is required") } return nil } // 验证消息 func (l *Aliyun) validateMessage(params interfaces.Message) error { if len(params.To) == 0 { return errors.New("at least one recipient is required") } if len(params.To) > MaxRecipients { return fmt.Errorf("too many recipients: %d (max: %d)", len(params.To), MaxRecipients) } if params.Subject == "" { return errors.New("subject is required") } if params.Body == "" { return errors.New("body is required") } return nil } // 发送邮件核心逻辑 func (l *Aliyun) sendEmail(ctx context.Context, params interfaces.Message) error { toAddress := strings.Join(params.To, ",") request := &dm20151123.SingleSendMailRequest{ AccountName: tea.String(l.Options.Aliyun.AccountName), ToAddress: tea.String(toAddress), Subject: tea.String(params.Subject), HtmlBody: tea.String(params.Body), AddressType: tea.Int32(0), // 随机账号 } // 设置回复地址 if params.ReplyTo != "" { request.ReplyToAddress = tea.Bool(false) request.ReplyAddress = tea.String(params.ReplyTo) } else if l.Options.Aliyun.ReplyAddress != "" { request.ReplyToAddress = tea.Bool(false) request.ReplyAddress = tea.String(l.Options.Aliyun.ReplyAddress) } else { request.ReplyToAddress = tea.Bool(true) } // 设置发件人名称 if params.Form != "" { request.FromAlias = tea.String(params.Form) } runtime := &util.RuntimeOptions{} resp, err := l.client.SingleSendMailWithOptions(request, runtime) if err != nil { return fmt.Errorf("Aliyun API call failed: %w", err) } // 记录响应信息(不包含敏感数据) if resp != nil && resp.Body != nil { l.Options.Logger.Infof(ctx, "Aliyun email sent - RequestId:%s EnvId:%s", tea.StringValue(resp.Body.RequestId), tea.StringValue(resp.Body.EnvId)) } return nil }