615 lines
18 KiB
Go
615 lines
18 KiB
Go
package email
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"embed"
|
|
"encoding/gob"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
cst2 "likedream-goframe/internal/common/cst"
|
|
db "likedream-goframe/internal/common/db"
|
|
"likedream-goframe/internal/ent/sysconfig"
|
|
"likedream-goframe/internal/framework/logger"
|
|
email2 "likedream-goframe/internal/framework/model/email"
|
|
"likedream-goframe/internal/framework/queue"
|
|
"likedream-goframe/internal/utils"
|
|
|
|
"github.com/gogf/gf/v2/os/gctx"
|
|
"github.com/hibiken/asynq"
|
|
mail "github.com/wneessen/go-mail"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
//go:embed templates/*.gohtml
|
|
var templateFiles embed.FS
|
|
|
|
const (
|
|
AuthModeSMTP = "smtp"
|
|
AuthModeOutlookOAuth2 = "outlook_oauth2"
|
|
|
|
DefaultOutlookSMTPHost = "smtp-mail.outlook.com"
|
|
DefaultOutlookSMTPPort = 587
|
|
DefaultOutlookTenant = "consumers"
|
|
DefaultOutlookOAuthScope = "offline_access https://graph.microsoft.com/Mail.Send"
|
|
DefaultOutlookRedirectURL = "http://localhost:3200/api/v1/mail/oauth/outlook/callback"
|
|
DefaultGraphSendMailURL = "https://graph.microsoft.com/v1.0/me/sendMail"
|
|
)
|
|
|
|
type EmailServiceV1 struct{}
|
|
|
|
type EmailConfig struct {
|
|
AuthMode string
|
|
|
|
SMTPHost string
|
|
SMTPPort int
|
|
SMTPUsername string
|
|
SMTPPassword string
|
|
|
|
FromEmail string
|
|
FromName string
|
|
|
|
OAuthTenant string
|
|
OAuthClientID string
|
|
OAuthClientSecret string
|
|
OAuthRedirectURL string
|
|
OAuthRefreshToken string
|
|
OAuthScope string
|
|
}
|
|
|
|
func NewV1() *EmailServiceV1 {
|
|
return &EmailServiceV1{}
|
|
}
|
|
|
|
func (s *EmailServiceV1) GetConfig() EmailConfig {
|
|
ctx := gctx.New()
|
|
|
|
getVal := func(key string) string {
|
|
config, err := db.Global.SysConfig.Query().
|
|
Where(sysconfig.KeyEQ(key)).
|
|
Only(ctx)
|
|
if err == nil {
|
|
return config.Value
|
|
}
|
|
return ""
|
|
}
|
|
|
|
port, _ := strconv.Atoi(getVal(cst2.ConfigEmailSMTPPort))
|
|
|
|
cfg := EmailConfig{
|
|
AuthMode: getVal(cst2.ConfigEmailAuthMode),
|
|
SMTPHost: getVal(cst2.ConfigEmailSMTPHost),
|
|
SMTPPort: port,
|
|
SMTPUsername: getVal(cst2.ConfigEmailSMTPUsername),
|
|
SMTPPassword: getVal(cst2.ConfigEmailSMTPPassword),
|
|
FromEmail: getVal(cst2.ConfigEmailFromEmail),
|
|
FromName: getVal(cst2.ConfigEmailFromName),
|
|
OAuthTenant: getVal(cst2.ConfigEmailOAuthTenant),
|
|
OAuthClientID: getVal(cst2.ConfigEmailOAuthClientID),
|
|
OAuthClientSecret: getVal(cst2.ConfigEmailOAuthClientSecret),
|
|
OAuthRedirectURL: getVal(cst2.ConfigEmailOAuthRedirectURL),
|
|
OAuthRefreshToken: getVal(cst2.ConfigEmailOAuthRefreshToken),
|
|
OAuthScope: getVal(cst2.ConfigEmailOAuthScope),
|
|
}
|
|
|
|
return NormalizeConfig(cfg)
|
|
}
|
|
|
|
func NormalizeConfig(cfg EmailConfig) EmailConfig {
|
|
if cfg.AuthMode == "" {
|
|
cfg.AuthMode = AuthModeSMTP
|
|
}
|
|
if cfg.FromEmail != "" && cfg.SMTPUsername == "" {
|
|
cfg.SMTPUsername = cfg.FromEmail
|
|
}
|
|
if cfg.AuthMode == AuthModeOutlookOAuth2 {
|
|
cfg.OAuthTenant = DefaultOutlookTenant
|
|
cfg.OAuthScope = DefaultOutlookOAuthScope
|
|
cfg.OAuthRedirectURL = DefaultOutlookRedirectURL
|
|
} else if cfg.SMTPPort == 0 {
|
|
cfg.SMTPPort = 587
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func (s *EmailServiceV1) SaveAuthorizedOutlookConfig(ctx context.Context, cfg EmailConfig) error {
|
|
cfg = NormalizeConfig(cfg)
|
|
|
|
configs := map[string]string{
|
|
cst2.ConfigEmailAuthMode: AuthModeOutlookOAuth2,
|
|
cst2.ConfigEmailFromEmail: cfg.FromEmail,
|
|
cst2.ConfigEmailOAuthTenant: cfg.OAuthTenant,
|
|
cst2.ConfigEmailOAuthClientID: cfg.OAuthClientID,
|
|
cst2.ConfigEmailOAuthClientSecret: cfg.OAuthClientSecret,
|
|
cst2.ConfigEmailOAuthRedirectURL: cfg.OAuthRedirectURL,
|
|
cst2.ConfigEmailOAuthRefreshToken: cfg.OAuthRefreshToken,
|
|
cst2.ConfigEmailOAuthScope: cfg.OAuthScope,
|
|
}
|
|
|
|
for key, value := range configs {
|
|
exists, err := db.Global.SysConfig.Query().Where(sysconfig.KeyEQ(key)).Exist(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
if _, err = db.Global.SysConfig.Update().Where(sysconfig.KeyEQ(key)).SetValue(value).Save(ctx); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if _, err = db.Global.SysConfig.Create().
|
|
SetKey(key).
|
|
SetValue(value).
|
|
SetDescription("邮件配置").
|
|
Save(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *EmailServiceV1) ValidateConfig(cfg EmailConfig) error {
|
|
cfg = NormalizeConfig(cfg)
|
|
|
|
if cfg.FromEmail == "" {
|
|
return fmt.Errorf("fromEmail is required")
|
|
}
|
|
if cfg.FromName == "" {
|
|
return fmt.Errorf("fromName is required")
|
|
}
|
|
|
|
switch cfg.AuthMode {
|
|
case AuthModeSMTP:
|
|
if cfg.SMTPHost == "" {
|
|
return fmt.Errorf("smtpHost is required")
|
|
}
|
|
if cfg.SMTPPort <= 0 {
|
|
return fmt.Errorf("smtpPort is required")
|
|
}
|
|
if cfg.SMTPUsername == "" {
|
|
return fmt.Errorf("smtpUsername is required")
|
|
}
|
|
if cfg.SMTPPassword == "" {
|
|
return fmt.Errorf("smtpPassword is required")
|
|
}
|
|
case AuthModeOutlookOAuth2:
|
|
if cfg.OAuthClientID == "" {
|
|
return fmt.Errorf("oauthClientId is required")
|
|
}
|
|
if cfg.OAuthClientSecret == "" {
|
|
return fmt.Errorf("oauthClientSecret is required")
|
|
}
|
|
if cfg.OAuthRefreshToken == "" {
|
|
return fmt.Errorf("oauthRefreshToken is required")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported authMode: %s", cfg.AuthMode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *EmailServiceV1) AccessToken(ctx context.Context, cfg EmailConfig) (string, error) {
|
|
cfg = NormalizeConfig(cfg)
|
|
if cfg.AuthMode != AuthModeOutlookOAuth2 {
|
|
return "", fmt.Errorf("access token is only available in oauth2 mode")
|
|
}
|
|
|
|
form := url.Values{
|
|
"client_id": {cfg.OAuthClientID},
|
|
"client_secret": {cfg.OAuthClientSecret},
|
|
"grant_type": {"refresh_token"},
|
|
"refresh_token": {cfg.OAuthRefreshToken},
|
|
"scope": {cfg.OAuthScope},
|
|
}
|
|
if cfg.OAuthRedirectURL != "" {
|
|
form.Set("redirect_uri", cfg.OAuthRedirectURL)
|
|
}
|
|
|
|
tokenURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", cfg.OAuthTenant)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var tokenResp struct {
|
|
AccessToken string `json:"access_token"`
|
|
Scope string `json:"scope"`
|
|
TokenType string `json:"token_type"`
|
|
ExpiresIn int64 `json:"expires_in"`
|
|
Error string `json:"error"`
|
|
ErrorDescription string `json:"error_description"`
|
|
}
|
|
if err := json.Unmarshal(bodyBytes, &tokenResp); err != nil {
|
|
return "", fmt.Errorf("decode oauth token response failed: %w", err)
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
if tokenResp.ErrorDescription != "" {
|
|
return "", fmt.Errorf("oauth token refresh failed: %s (%s)", tokenResp.ErrorDescription, tokenResp.Error)
|
|
}
|
|
return "", fmt.Errorf("oauth token refresh failed with status %d", resp.StatusCode)
|
|
}
|
|
if tokenResp.AccessToken == "" {
|
|
return "", fmt.Errorf("empty access token returned by oauth2 token endpoint")
|
|
}
|
|
return strings.TrimSpace(tokenResp.AccessToken), nil
|
|
}
|
|
|
|
func (s *EmailServiceV1) newClient(ctx context.Context, cfg EmailConfig) (*mail.Client, error) {
|
|
cfg = NormalizeConfig(cfg)
|
|
if err := s.ValidateConfig(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.AuthMode != AuthModeSMTP {
|
|
return nil, fmt.Errorf("smtp client is only available in smtp mode")
|
|
}
|
|
|
|
options := []mail.Option{
|
|
mail.WithPort(cfg.SMTPPort),
|
|
mail.WithTLSPortPolicy(mail.TLSMandatory),
|
|
mail.WithTimeout(30 * time.Second),
|
|
mail.WithUsername(cfg.SMTPUsername),
|
|
}
|
|
|
|
options = append(options,
|
|
mail.WithSMTPAuth(mail.SMTPAuthLogin),
|
|
mail.WithPassword(cfg.SMTPPassword),
|
|
)
|
|
|
|
return mail.NewClient(cfg.SMTPHost, options...)
|
|
}
|
|
|
|
func (s *EmailServiceV1) newMessage(fromName, fromEmail, to, subject, body string) (*mail.Msg, error) {
|
|
msg := mail.NewMsg()
|
|
if err := msg.FromFormat(fromName, fromEmail); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := msg.AddTo(to); err != nil {
|
|
return nil, err
|
|
}
|
|
msg.Subject(subject)
|
|
msg.SetBodyString(mail.TypeTextHTML, body)
|
|
return msg, nil
|
|
}
|
|
|
|
func (s *EmailServiceV1) SendMessageNow(ctx context.Context, cfg EmailConfig, to, subject, body string) error {
|
|
cfg = NormalizeConfig(cfg)
|
|
if err := s.ValidateConfig(cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
if cfg.AuthMode == AuthModeOutlookOAuth2 {
|
|
return s.sendGraphMessageNow(ctx, cfg, to, subject, body)
|
|
}
|
|
|
|
msg, err := s.newMessage(cfg.FromName, cfg.FromEmail, to, subject, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := s.newClient(ctx, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
return client.DialAndSendWithContext(ctx, msg)
|
|
}
|
|
|
|
func (s *EmailServiceV1) sendGraphMessageNow(ctx context.Context, cfg EmailConfig, to, subject, body string) error {
|
|
accessToken, err := s.AccessToken(ctx, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"message": map[string]any{
|
|
"subject": subject,
|
|
"body": map[string]any{
|
|
"contentType": "HTML",
|
|
"content": body,
|
|
},
|
|
"toRecipients": []map[string]any{
|
|
{
|
|
"emailAddress": map[string]any{
|
|
"address": to,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"saveToSentItems": true,
|
|
}
|
|
|
|
bodyBytes, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, DefaultGraphSendMailURL, bytes.NewReader(bodyBytes))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
var graphErr struct {
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&graphErr)
|
|
if graphErr.Error.Message != "" {
|
|
return fmt.Errorf("graph sendMail failed: %s (%s)", graphErr.Error.Message, graphErr.Error.Code)
|
|
}
|
|
return fmt.Errorf("graph sendMail failed with status %d", resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ConfigFromPayload(payload email2.SendPayload) EmailConfig {
|
|
return NormalizeConfig(EmailConfig{
|
|
AuthMode: payload.AuthMode,
|
|
SMTPHost: payload.SMTPHost,
|
|
SMTPPort: payload.SMTPPort,
|
|
SMTPUsername: payload.SMTPUsername,
|
|
SMTPPassword: payload.SMTPPassword,
|
|
FromEmail: payload.FromEmail,
|
|
FromName: payload.FromName,
|
|
OAuthTenant: payload.OAuthTenant,
|
|
OAuthClientID: payload.OAuthClientID,
|
|
OAuthClientSecret: payload.OAuthClientSecret,
|
|
OAuthRedirectURL: payload.OAuthRedirectURL,
|
|
OAuthRefreshToken: payload.OAuthRefreshToken,
|
|
OAuthScope: payload.OAuthScope,
|
|
})
|
|
}
|
|
|
|
func PayloadFromConfig(cfg EmailConfig, to, subject, body string) *email2.SendPayload {
|
|
cfg = NormalizeConfig(cfg)
|
|
return &email2.SendPayload{
|
|
To: to,
|
|
Subject: subject,
|
|
Body: body,
|
|
AuthMode: cfg.AuthMode,
|
|
FromName: cfg.FromName,
|
|
FromEmail: cfg.FromEmail,
|
|
SMTPHost: cfg.SMTPHost,
|
|
SMTPPort: cfg.SMTPPort,
|
|
SMTPUsername: cfg.SMTPUsername,
|
|
SMTPPassword: cfg.SMTPPassword,
|
|
OAuthTenant: cfg.OAuthTenant,
|
|
OAuthClientID: cfg.OAuthClientID,
|
|
OAuthClientSecret: cfg.OAuthClientSecret,
|
|
OAuthRedirectURL: cfg.OAuthRedirectURL,
|
|
OAuthRefreshToken: cfg.OAuthRefreshToken,
|
|
OAuthScope: cfg.OAuthScope,
|
|
}
|
|
}
|
|
|
|
func templatePrefix() string {
|
|
if IsNewBrand() {
|
|
return "new_"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func brandAppName() string {
|
|
return GetBrandConfig().AppName
|
|
}
|
|
|
|
func brandCopyright() string {
|
|
return GetBrandConfig().AppCopyright
|
|
}
|
|
|
|
func (s *EmailServiceV1) SendSignupEmail(ctx context.Context, toEmail, username string, token string) *utils.Response[any] {
|
|
cfg := s.GetConfig()
|
|
if err := s.ValidateConfig(cfg); err != nil {
|
|
logger.Logger.Error("mail config is invalid", zap.Error(err))
|
|
return utils.FailBusiness("邮件服务未配置")
|
|
}
|
|
|
|
subject := fmt.Sprintf("欢迎注册%s", brandAppName())
|
|
body, err := s.renderSignupEmailTemplate(username, token)
|
|
if err != nil {
|
|
logger.Logger.Error("render signup email failed", zap.Error(err))
|
|
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
|
}
|
|
|
|
if err = s.sendEmail(ctx, toEmail, subject, body); err != nil {
|
|
logger.Logger.Error("send signup email failed", zap.Error(err))
|
|
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
|
}
|
|
|
|
return utils.SuccessWithMsg("注册邮件发送成功")
|
|
}
|
|
|
|
func (s *EmailServiceV1) SendTaskNotificationEmail(ctx context.Context, toEmail, taskName string, status email2.TaskStatus, details map[string]string) *utils.Response[any] {
|
|
cfg := s.GetConfig()
|
|
if err := s.ValidateConfig(cfg); err != nil {
|
|
return utils.FailBusiness("邮件服务未配置")
|
|
}
|
|
|
|
subject := fmt.Sprintf("任务状态通知: %s", taskName)
|
|
body, err := s.renderTaskNotificationTemplate(taskName, status, details)
|
|
if err != nil {
|
|
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
|
}
|
|
|
|
if err = s.sendEmail(ctx, toEmail, subject, body); err != nil {
|
|
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
|
}
|
|
|
|
return utils.SuccessWithMsg("通知邮件发送成功")
|
|
}
|
|
|
|
func (s *EmailServiceV1) SendResetPasswordEmail(ctx context.Context, toEmail, username string, token string) *utils.Response[any] {
|
|
cfg := s.GetConfig()
|
|
if err := s.ValidateConfig(cfg); err != nil {
|
|
logger.Logger.Error("mail config is invalid", zap.Error(err))
|
|
return utils.FailBusiness("邮件服务未配置")
|
|
}
|
|
|
|
subject := fmt.Sprintf("重置%s账户密码", brandAppName())
|
|
body, err := s.renderResetPasswordEmailTemplate(username, token)
|
|
if err != nil {
|
|
logger.Logger.Error("render reset password email failed", zap.Error(err))
|
|
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
|
}
|
|
|
|
if err = s.sendEmail(ctx, toEmail, subject, body); err != nil {
|
|
logger.Logger.Error("send reset password email failed", zap.Error(err))
|
|
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
|
}
|
|
|
|
return utils.SuccessWithMsg("重置密码邮件发送成功")
|
|
}
|
|
|
|
func (s *EmailServiceV1) SendBalanceMigrateEmail(ctx context.Context, toEmail, username string, balance int, token string) *utils.Response[any] {
|
|
cfg := s.GetConfig()
|
|
if err := s.ValidateConfig(cfg); err != nil {
|
|
logger.Logger.Error("mail config is invalid", zap.Error(err))
|
|
return utils.FailBusiness("邮件服务未配置")
|
|
}
|
|
|
|
subject := fmt.Sprintf("%s积分转移通知", brandAppName())
|
|
body, err := s.renderBalanceMigrateEmailTemplate(username, balance, token)
|
|
if err != nil {
|
|
logger.Logger.Error("render balance migrate email failed", zap.Error(err))
|
|
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
|
}
|
|
|
|
if err = s.sendEmail(ctx, toEmail, subject, body); err != nil {
|
|
logger.Logger.Error("send balance migrate email failed", zap.Error(err))
|
|
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
|
}
|
|
|
|
return utils.SuccessWithMsg("积分转移邮件发送成功")
|
|
}
|
|
|
|
func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string) error {
|
|
cfg := s.GetConfig()
|
|
if err := s.ValidateConfig(cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
payload := bytes.NewBuffer(nil)
|
|
if err := gob.NewEncoder(payload).Encode(PayloadFromConfig(cfg, to, subject, body)); err != nil {
|
|
return err
|
|
}
|
|
|
|
task := queue.AddTask(
|
|
ctx,
|
|
asynq.NewTask(cst2.TaskEmail, payload.Bytes()),
|
|
asynq.Queue(cst2.TaskQueueEmail),
|
|
asynq.MaxRetry(1),
|
|
asynq.Timeout(5*time.Minute),
|
|
)
|
|
logger.Logger.Info("add email task", zap.String("task_id", task.ID), zap.String("to", to))
|
|
return nil
|
|
}
|
|
|
|
func parseTemplates(contentTemplate string) (*template.Template, error) {
|
|
prefix := templatePrefix()
|
|
layoutFile := fmt.Sprintf("templates/%slayout.gohtml", prefix)
|
|
contentFile := fmt.Sprintf("templates/%s%s", prefix, contentTemplate)
|
|
return template.ParseFS(templateFiles, layoutFile, contentFile)
|
|
}
|
|
|
|
func (s *EmailServiceV1) renderSignupEmailTemplate(username string, token string) (string, error) {
|
|
tmpl, err := parseTemplates("signup_confirm.gohtml")
|
|
if err != nil {
|
|
return "", fmt.Errorf("解析注册邮件模板失败: %v", err)
|
|
}
|
|
|
|
bc := GetBrandConfig()
|
|
data := NewSignupTemplateData(username, bc.AppName, bc.AppCopyright, bc.EmailConfirmBaseUrl+token)
|
|
|
|
prefix := templatePrefix()
|
|
var buf bytes.Buffer
|
|
if err = tmpl.ExecuteTemplate(&buf, prefix+"signup_confirm.gohtml", data); err != nil {
|
|
return "", fmt.Errorf("渲染注册邮件模板失败: %v", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func (s *EmailServiceV1) renderTaskNotificationTemplate(taskName string, status email2.TaskStatus, details map[string]string) (string, error) {
|
|
tmpl, err := parseTemplates("task_notification.gohtml")
|
|
if err != nil {
|
|
return "", fmt.Errorf("解析任务通知邮件模板失败: %v", err)
|
|
}
|
|
|
|
bc := GetBrandConfig()
|
|
data := NewTaskNotificationTemplateData(taskName, status, details, bc.AppName, bc.AppCopyright)
|
|
|
|
prefix := templatePrefix()
|
|
var buf bytes.Buffer
|
|
if err = tmpl.ExecuteTemplate(&buf, prefix+"task_notification.gohtml", data); err != nil {
|
|
return "", fmt.Errorf("渲染任务通知邮件模板失败: %v", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func (s *EmailServiceV1) renderResetPasswordEmailTemplate(username string, token string) (string, error) {
|
|
tmpl, err := parseTemplates("reset_password.gohtml")
|
|
if err != nil {
|
|
return "", fmt.Errorf("解析重置密码邮件模板失败: %v", err)
|
|
}
|
|
|
|
bc := GetBrandConfig()
|
|
data := NewResetPasswordTemplateData(username, bc.AppName, bc.AppCopyright, bc.EmailResetPasswordUrl+token)
|
|
|
|
prefix := templatePrefix()
|
|
var buf bytes.Buffer
|
|
if err = tmpl.ExecuteTemplate(&buf, prefix+"reset_password.gohtml", data); err != nil {
|
|
return "", fmt.Errorf("渲染重置密码邮件模板失败: %v", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func (s *EmailServiceV1) renderBalanceMigrateEmailTemplate(username string, balance int, token string) (string, error) {
|
|
tmpl, err := parseTemplates("balance_migrate.gohtml")
|
|
if err != nil {
|
|
return "", fmt.Errorf("解析积分转移邮件模板失败: %v", err)
|
|
}
|
|
|
|
bc := GetBrandConfig()
|
|
data := NewBalanceMigrateTemplateData(username, bc.AppName, bc.AppCopyright, balance, bc.BalanceMigrateUrl+token)
|
|
|
|
prefix := templatePrefix()
|
|
var buf bytes.Buffer
|
|
if err = tmpl.ExecuteTemplate(&buf, prefix+"balance_migrate.gohtml", data); err != nil {
|
|
return "", fmt.Errorf("渲染积分转移邮件模板失败: %v", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|