fix: api descript
This commit is contained in:
@@ -16,3 +16,29 @@ Kmq:
|
||||
Offset: last
|
||||
Consumers: 8
|
||||
Processors: 8
|
||||
|
||||
Mail:
|
||||
Enabled: true
|
||||
Host: "${EMAIL_SMTP_HOST}"
|
||||
Port: ${EMAIL_SMTP_PORT}
|
||||
Username: "${EMAIL_SMTP_USERNAME}"
|
||||
Password: "${EMAIL_SMTP_PASSWORD}"
|
||||
FromAddress: "${EMAIL_FROM_ADDRESS}"
|
||||
FromName: "${EMAIL_FROM_NAME}"
|
||||
UseSSL: true
|
||||
UseStartTLS: false
|
||||
InsecureSkipVerify: false
|
||||
ReplyTo: "${EMAIL_REPLY_TO}"
|
||||
|
||||
# Mail:
|
||||
# Enabled: true
|
||||
# Host: "smtp.163.com"
|
||||
# Port: 465
|
||||
# Username: "churong2646@163.com"
|
||||
# Password: "GTv6C6qNbv5urAiD"
|
||||
# FromAddress: "churong2646@163.com"
|
||||
# FromName: "聚玩"
|
||||
# UseSSL: true
|
||||
# UseStartTLS: false
|
||||
# InsecureSkipVerify: false
|
||||
# ReplyTo: ""
|
||||
|
||||
@@ -7,5 +7,20 @@ import (
|
||||
|
||||
type Config struct {
|
||||
service.ServiceConf
|
||||
Kmq kq.KqConf
|
||||
Kmq kq.KqConf
|
||||
Mail MailConf
|
||||
}
|
||||
|
||||
type MailConf struct {
|
||||
Enabled bool `json:",optional"`
|
||||
Host string `json:",optional"`
|
||||
Port int `json:",optional"`
|
||||
Username string `json:",optional"`
|
||||
Password string `json:",optional"`
|
||||
FromAddress string `json:",optional"`
|
||||
FromName string `json:",optional"`
|
||||
UseSSL bool `json:",optional"`
|
||||
UseStartTLS bool `json:",optional"`
|
||||
InsecureSkipVerify bool `json:",optional"`
|
||||
ReplyTo string `json:",optional"`
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@ package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"juwan-backend/app/email/mq/internal/config"
|
||||
"juwan-backend/app/email/mq/internal/mailer"
|
||||
"juwan-backend/app/email/mq/internal/svc"
|
||||
"strings"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
@@ -23,10 +27,59 @@ func NewSendVerificationCodeMq(ctx context.Context, c config.Config, svcCtx *svc
|
||||
}
|
||||
|
||||
func (l *SendVerificationCodeMq) Consume(ctx context.Context, key, value string) error {
|
||||
_ = ctx
|
||||
_ = key
|
||||
_ = value
|
||||
logx.Infof("Consume get message key: %s, value: %s", key, value)
|
||||
if l.svcCxt.MailSender == nil {
|
||||
return fmt.Errorf("mail sender not initialized")
|
||||
}
|
||||
|
||||
var payload verificationCodePayload
|
||||
if err := json.Unmarshal([]byte(value), &payload); err != nil {
|
||||
logx.Errorf("failed to unmarshal verification code payload: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.Type != "verification_code" {
|
||||
logx.Infof("skip unsupported email task type: %s", payload.Type)
|
||||
return nil
|
||||
}
|
||||
|
||||
emailAddr := strings.TrimSpace(payload.Email)
|
||||
code := strings.TrimSpace(payload.Code)
|
||||
scene := strings.TrimSpace(payload.Scene)
|
||||
if emailAddr == "" || code == "" {
|
||||
logx.Errorf("invalid verification payload: email=%s, code=%s", emailAddr, code)
|
||||
return fmt.Errorf("invalid verification payload: email/code is required")
|
||||
}
|
||||
|
||||
expireIn := payload.ExpireIn
|
||||
if expireIn <= 0 {
|
||||
expireIn = 60
|
||||
}
|
||||
|
||||
subject := "Your verification code"
|
||||
body := fmt.Sprintf("Your verification code is %s. It is valid for %d seconds. Scene: %s. RequestId: %s", code, expireIn, scene, payload.RequestID)
|
||||
// logx.Info("Send email to address: %s, subject: %s", emailAddr, subject)
|
||||
|
||||
err := l.svcCxt.MailSender.Send(ctx, mailer.Message{
|
||||
To: []string{emailAddr},
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
IsHTML: false,
|
||||
})
|
||||
if err != nil {
|
||||
logx.Errorf("failed to send verification email to %s: %v", emailAddr, err)
|
||||
return err
|
||||
}
|
||||
|
||||
logx.Infof("verification email sent to %s successfully", emailAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
type verificationCodePayload struct {
|
||||
Type string `json:"type"`
|
||||
RequestID string `json:"requestId"`
|
||||
Email string `json:"email"`
|
||||
Scene string `json:"scene"`
|
||||
Code string `json:"code"`
|
||||
ExpireIn int64 `json:"expireIn"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"juwan-backend/app/email/mq/internal/config"
|
||||
)
|
||||
|
||||
type Sender struct {
|
||||
conf config.MailConf
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
To []string
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Subject string
|
||||
Body string
|
||||
IsHTML bool
|
||||
}
|
||||
|
||||
func NewSender(conf config.MailConf) (*Sender, error) {
|
||||
if strings.TrimSpace(conf.Host) == "" {
|
||||
return nil, fmt.Errorf("mail host is required")
|
||||
}
|
||||
if conf.Port <= 0 {
|
||||
return nil, fmt.Errorf("mail port is required")
|
||||
}
|
||||
if strings.TrimSpace(conf.FromAddress) == "" {
|
||||
return nil, fmt.Errorf("mail from address is required")
|
||||
}
|
||||
if conf.UseSSL && conf.UseStartTLS {
|
||||
return nil, fmt.Errorf("mail config invalid: UseSSL and UseStartTLS cannot both be true")
|
||||
}
|
||||
|
||||
return &Sender{conf: conf}, nil
|
||||
}
|
||||
|
||||
func (s *Sender) Send(ctx context.Context, msg Message) error {
|
||||
toList := compactAddresses(msg.To)
|
||||
if len(toList) == 0 {
|
||||
return fmt.Errorf("mail recipients are empty")
|
||||
}
|
||||
|
||||
ccList := compactAddresses(msg.Cc)
|
||||
bccList := compactAddresses(msg.Bcc)
|
||||
allRecipients := append(append([]string{}, toList...), ccList...)
|
||||
allRecipients = append(allRecipients, bccList...)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", s.conf.Host, s.conf.Port)
|
||||
|
||||
var (
|
||||
client *smtp.Client
|
||||
err error
|
||||
)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: s.conf.Host,
|
||||
InsecureSkipVerify: s.conf.InsecureSkipVerify,
|
||||
}
|
||||
|
||||
if s.conf.UseSSL {
|
||||
conn, dialErr := tls.DialWithDialer((&net.Dialer{}), "tcp", addr, tlsConfig)
|
||||
if dialErr != nil {
|
||||
return fmt.Errorf("smtp ssl dial failed(%s): %w", addr, dialErr)
|
||||
}
|
||||
client, err = smtp.NewClient(conn, s.conf.Host)
|
||||
} else {
|
||||
dialer := &net.Dialer{}
|
||||
conn, dialErr := dialer.DialContext(ctx, "tcp", addr)
|
||||
if dialErr != nil {
|
||||
return fmt.Errorf("smtp dial failed(%s): %w", addr, dialErr)
|
||||
}
|
||||
client, err = smtp.NewClient(conn, s.conf.Host)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp create client failed: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if s.conf.UseStartTLS {
|
||||
if err = client.StartTLS(tlsConfig); err != nil {
|
||||
return fmt.Errorf("smtp starttls failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(s.conf.Username) != "" {
|
||||
auth := smtp.PlainAuth("", s.conf.Username, s.conf.Password, s.conf.Host)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("smtp auth failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = client.Mail(s.conf.FromAddress); err != nil {
|
||||
return fmt.Errorf("smtp mail from failed: %w", err)
|
||||
}
|
||||
for _, rcpt := range allRecipients {
|
||||
if err = client.Rcpt(rcpt); err != nil {
|
||||
return fmt.Errorf("smtp rcpt to(%s) failed: %w", rcpt, err)
|
||||
}
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp data start failed: %w", err)
|
||||
}
|
||||
|
||||
bodyType := "text/plain; charset=UTF-8"
|
||||
if msg.IsHTML {
|
||||
bodyType = "text/html; charset=UTF-8"
|
||||
}
|
||||
|
||||
headers := []string{
|
||||
fmt.Sprintf("From: %s", formatFrom(s.conf.FromName, s.conf.FromAddress)),
|
||||
fmt.Sprintf("To: %s", strings.Join(toList, ",")),
|
||||
fmt.Sprintf("Subject: %s", msg.Subject),
|
||||
"MIME-Version: 1.0",
|
||||
fmt.Sprintf("Content-Type: %s", bodyType),
|
||||
}
|
||||
if len(ccList) > 0 {
|
||||
headers = append(headers, fmt.Sprintf("Cc: %s", strings.Join(ccList, ",")))
|
||||
}
|
||||
if strings.TrimSpace(s.conf.ReplyTo) != "" {
|
||||
headers = append(headers, fmt.Sprintf("Reply-To: %s", strings.TrimSpace(s.conf.ReplyTo)))
|
||||
}
|
||||
|
||||
raw := strings.Join(headers, "\r\n") + "\r\n\r\n" + msg.Body
|
||||
if _, err = w.Write([]byte(raw)); err != nil {
|
||||
_ = w.Close()
|
||||
return fmt.Errorf("smtp write body failed: %w", err)
|
||||
}
|
||||
if err = w.Close(); err != nil {
|
||||
return fmt.Errorf("smtp data close failed: %w", err)
|
||||
}
|
||||
|
||||
if err = client.Quit(); err != nil {
|
||||
return fmt.Errorf("smtp quit failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatFrom(name, address string) string {
|
||||
trimmedName := strings.TrimSpace(name)
|
||||
trimmedAddress := strings.TrimSpace(address)
|
||||
if trimmedName == "" {
|
||||
return trimmedAddress
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s <%s>", trimmedName, trimmedAddress)
|
||||
}
|
||||
|
||||
func compactAddresses(input []string) []string {
|
||||
result := make([]string, 0, len(input))
|
||||
for _, item := range input {
|
||||
trimmed := strings.TrimSpace(item)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,13 +1,30 @@
|
||||
package svc
|
||||
|
||||
import "juwan-backend/app/email/mq/internal/config"
|
||||
import (
|
||||
"juwan-backend/app/email/mq/internal/config"
|
||||
"juwan-backend/app/email/mq/internal/mailer"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type ServiceContext struct {
|
||||
c config.Config
|
||||
c config.Config
|
||||
MailSender *mailer.Sender
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
var sender *mailer.Sender
|
||||
if c.Mail.Enabled {
|
||||
mailSender, err := mailer.NewSender(c.Mail)
|
||||
if err != nil {
|
||||
logx.Errorf("failed to init mail sender: %v", err)
|
||||
} else {
|
||||
sender = mailSender
|
||||
}
|
||||
}
|
||||
|
||||
return &ServiceContext{
|
||||
c: c,
|
||||
c: c,
|
||||
MailSender: sender,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user