193 lines
4.4 KiB
Go
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
|
|
}
|