thehrz 0b880bece8
All checks were successful
CI / deploy (push) Successful in 57s
pref: rename project
2025-01-24 17:12:15 +08:00

193 lines
4.4 KiB
Go

package email
import (
"bytes"
"context"
"crypto/rsa"
"errors"
"fmt"
"html/template"
"math/rand"
"net/url"
"time"
"tinyskin/config"
"tinyskin/db/cache"
"github.com/golang-jwt/jwt/v5"
"github.com/samber/lo"
"github.com/wneessen/go-mail"
)
type EmailConfig struct {
Host string
Port int
SSL bool
Name string
Pass string
}
type EmailService struct {
emailConfig []EmailConfig
pri *rsa.PrivateKey
config config.Config
cache cache.Cache
}
func NewEmail(pri *rsa.PrivateKey, c config.Config, cache cache.Cache) (*EmailService, error) {
ec := lo.Map[config.SmtpUser, EmailConfig](c.Email.Smtp, func(item config.SmtpUser, index int) EmailConfig {
return EmailConfig{
Host: item.Host,
Port: item.Port,
SSL: item.SSL,
Name: item.Name,
Pass: item.Pass,
}
})
return &EmailService{
emailConfig: ec,
pri: pri,
config: c,
cache: cache,
}, nil
}
func (e EmailService) getRandEmailUser() (EmailConfig, error) {
if len(e.emailConfig) == 0 {
return EmailConfig{}, fmt.Errorf("没有可用的邮箱账号")
}
i := rand.Intn(len(e.emailConfig))
return e.emailConfig[i], nil
}
func (e EmailService) SendEmail(ctx context.Context, to string, subject, body string) error {
u, err := e.getRandEmailUser()
if err != nil {
return fmt.Errorf("SendRegVerify: %w", err)
}
m := mail.NewMsg()
err = m.From(u.Name)
if err != nil {
return fmt.Errorf("SendRegVerify: %w", err)
}
err = m.To(to)
if err != nil {
return fmt.Errorf("SendRegVerify: %w", err)
}
m.Subject(subject)
m.SetBodyString(mail.TypeTextHTML, body)
c, err := mail.NewClient(u.Host, mail.WithPort(u.Port), mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(u.Name), mail.WithPassword(u.Pass))
if err != nil {
return fmt.Errorf("SendRegVerify: %w", err)
}
if u.SSL {
c.SetSSL(true)
}
defer c.Close()
err = c.DialAndSendWithContext(ctx, m)
if err != nil {
return fmt.Errorf("SendRegVerify: %w", err)
}
return nil
}
var emailTemplate = lo.Must(template.New("email").Parse(`<p>{{ .msg }}</p><a href="{{.url}}">{{ .url }}</a>`))
func (e EmailService) SendVerifyUrl(ctx context.Context, email string, interval int, host string, subject, msg, path string) error {
sendKey := []byte("SendEmail" + email)
sendB, err := e.cache.Get(sendKey)
if err != nil {
return fmt.Errorf("SendVerifyUrl: %w", err)
}
if sendB != nil {
return fmt.Errorf("SendVerifyUrl: %w", ErrSendLimit)
}
err = e.cache.Put(sendKey, []byte{1}, time.Now().Add(time.Second*time.Duration(interval)))
if err != nil {
return fmt.Errorf("SendVerifyUrl: %w", err)
}
code, err := newJwtToken(e.pri, email, issuer+path)
if err != nil {
return fmt.Errorf("SendVerifyUrl: %w", err)
}
q := url.Values{}
q.Set("code", code)
q.Set("email", email)
u := url.URL{
Host: host,
Scheme: "http",
Path: path,
}
u.RawQuery = q.Encode()
if e.config.WebBaseUrl != "" {
webBase, err := url.Parse(e.config.WebBaseUrl)
if err != nil {
return fmt.Errorf("SendVerifyUrl: %w", err)
}
u.Host = webBase.Host
u.Scheme = webBase.Scheme
}
body := bytes.NewBuffer(nil)
err = emailTemplate.Execute(body, map[string]any{
"msg": msg,
"url": u.String(),
})
if err != nil {
return fmt.Errorf("SendVerifyUrl: %w", err)
}
err = e.SendEmail(ctx, email, subject, body.String())
if err != nil {
return fmt.Errorf("SendVerifyUrl: %w", err)
}
return nil
}
var (
ErrSendLimit = errors.New("邮件发送限制")
ErrTokenInvalid = errors.New("token 无效")
)
func (e EmailService) VerifyJwt(email, jwtStr, path string) error {
token, err := jwt.ParseWithClaims(jwtStr, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
return &e.pri.PublicKey, nil
})
if err != nil {
return fmt.Errorf("VerifyJwt: %w", err)
}
sub, _ := token.Claims.GetSubject()
iss, _ := token.Claims.GetIssuer()
if !token.Valid || sub != email || issuer+path != iss {
return fmt.Errorf("VerifyJwt: %w", ErrTokenInvalid)
}
return nil
}
const issuer = "email"
func newJwtToken(jwtKey *rsa.PrivateKey, email, iss string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * 24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: email,
Issuer: iss,
})
jwts, err := token.SignedString(jwtKey)
if err != nil {
return "", fmt.Errorf("newJwtToken: %w", err)
}
return jwts, nil
}