You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
synctv/internal/email/email.go

378 lines
7.3 KiB
Go

package email
import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"strings"
"text/template"
"time"
"github.com/Boostport/mjml-go"
log "github.com/sirupsen/logrus"
"github.com/synctv-org/synctv/internal/email/emailtemplate"
"github.com/synctv-org/synctv/internal/model"
"github.com/synctv-org/synctv/internal/settings"
"github.com/synctv-org/synctv/utils"
"github.com/zijiren233/gencontainer/synccache"
"github.com/zijiren233/stream"
)
var (
ErrEmailNotEnabled = errors.New("email is not enabled")
emailCaptcha *synccache.SyncCache[string, string] = synccache.NewSyncCache[string, string](time.Minute * 5)
)
var (
EnableEmail = settings.NewBoolSetting(
"enable_email",
false,
model.SettingGroupEmail,
settings.WithAfterSetBool(func(bs settings.BoolSetting, b bool) {
if !b {
closeSmtpPool()
}
}),
)
DisableUserSignup = settings.NewBoolSetting(
"email_disable_user_signup",
false,
model.SettingGroupEmail,
)
SignupNeedReview = settings.NewBoolSetting(
"email_signup_need_review",
false,
model.SettingGroupEmail,
)
EmailSignupWhiteListEnable = settings.NewBoolSetting(
"email_signup_white_list_enable",
false,
model.SettingGroupEmail,
)
EmailSignupWhiteList = settings.NewStringSetting(
"email_signup_white_list",
`gmail.com,qq.com,163.com,yahoo.com,sina.com,126.com,outlook.com,yeah.net,foxmail.com`,
model.SettingGroupEmail,
)
)
var (
testTemplate *template.Template
captchaTemplate *template.Template
retrievePasswordTemplate *template.Template
)
func init() {
body, err := mjml.ToHTML(
context.Background(),
stream.BytesToString(emailtemplate.TestMjml),
mjml.WithMinify(true),
)
if err != nil {
log.Fatalf("mjml test template error: %v", err)
}
t, err := template.New("").Parse(body)
if err != nil {
log.Fatalf("parse test template error: %v", err)
}
testTemplate = t
body, err = mjml.ToHTML(
context.Background(),
stream.BytesToString(emailtemplate.CaptchaMjml),
mjml.WithMinify(true),
)
if err != nil {
log.Fatalf("mjml captcha template error: %v", err)
}
t, err = template.New("").Parse(body)
if err != nil {
log.Fatalf("parse captcha template error: %v", err)
}
captchaTemplate = t
body, err = mjml.ToHTML(
context.Background(),
stream.BytesToString(emailtemplate.RetrievePasswordMjml),
mjml.WithMinify(true),
)
if err != nil {
log.Fatalf("mjml retrieve password template error: %v", err)
}
t, err = template.New("").Parse(body)
if err != nil {
log.Fatalf("parse retrieve password template error: %v", err)
}
retrievePasswordTemplate = t
}
type testPayload struct {
Username string
Year int
}
type captchaPayload struct {
Captcha string
Year int
}
type retrievePasswordPayload struct {
Captcha string
Host string
URL string
Year int
}
func SendBindCaptchaEmail(userID, userEmail string) error {
if !EnableEmail.Get() {
return ErrEmailNotEnabled
}
if userID == "" {
return errors.New("user id is empty")
}
if userEmail == "" {
return errors.New("email is empty")
}
pool, err := getSmtpPool()
if err != nil {
return err
}
entry, loaded := emailCaptcha.LoadOrStore(
fmt.Sprintf("bind:%s:%s", userID, userEmail),
utils.RandString(6),
time.Minute*5,
)
if loaded {
entry.SetExpiration(time.Now().Add(time.Minute * 5))
}
out := bytes.NewBuffer(nil)
err = captchaTemplate.Execute(out, captchaPayload{
Captcha: entry.Value(),
Year: time.Now().Year(),
})
if err != nil {
return err
}
return pool.SendEmail(
[]string{userEmail},
"SyncTV Verification Code",
out.String(),
)
}
func VerifyBindCaptchaEmail(userID, userEmail, captcha string) (bool, error) {
if !EnableEmail.Get() {
return false, ErrEmailNotEnabled
}
if userID == "" {
return false, errors.New("user id is empty")
}
if userEmail == "" {
return false, errors.New("email is empty")
}
if captcha == "" {
return false, errors.New("captcha is empty")
}
key := fmt.Sprintf("bind:%s:%s", userID, userEmail)
if emailCaptcha.CompareValueAndDelete(
key,
captcha,
) {
return true, nil
}
return false, nil
}
func SendTestEmail(username, email string) error {
if email == "" {
return errors.New("email is empty")
}
pool, err := getSmtpPool()
if err != nil {
return err
}
out := bytes.NewBuffer(nil)
err = testTemplate.Execute(out, testPayload{
Username: username,
Year: time.Now().Year(),
})
if err != nil {
return err
}
return pool.SendEmail(
[]string{email},
"SyncTV Test Email",
out.String(),
)
}
func SendSignupCaptchaEmail(email string) error {
if !EnableEmail.Get() {
return ErrEmailNotEnabled
}
if email == "" {
return errors.New("email is empty")
}
pool, err := getSmtpPool()
if err != nil {
return err
}
entry, loaded := emailCaptcha.LoadOrStore(
"signup:"+email,
utils.RandString(6),
time.Minute*5,
)
if loaded {
entry.SetExpiration(time.Now().Add(time.Minute * 5))
}
out := bytes.NewBuffer(nil)
err = captchaTemplate.Execute(out, captchaPayload{
Captcha: entry.Value(),
Year: time.Now().Year(),
})
if err != nil {
return err
}
return pool.SendEmail(
[]string{email},
"SyncTV Signup Verification Code",
out.String(),
)
}
func VerifySignupCaptchaEmail(email, captcha string) (bool, error) {
if !EnableEmail.Get() {
return false, ErrEmailNotEnabled
}
if email == "" {
return false, errors.New("email is empty")
}
if captcha == "" {
return false, errors.New("captcha is empty")
}
if emailCaptcha.CompareValueAndDelete(
"signup:"+email,
captcha,
) {
return true, nil
}
return false, nil
}
func SendRetrievePasswordCaptchaEmail(userID, email, host string) error {
if !EnableEmail.Get() {
return ErrEmailNotEnabled
}
if userID == "" {
return errors.New("user id is empty")
}
if email == "" {
return errors.New("email is empty")
}
if host == "" {
return errors.New("host is empty")
}
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
log.Errorf("host: %s must start with http:// or https://", host)
return errors.New("get host error")
}
u, err := url.Parse(host)
if err != nil {
return err
}
u.Path = `web/auth/reset`
pool, err := getSmtpPool()
if err != nil {
return err
}
entry, loaded := emailCaptcha.LoadOrStore(
fmt.Sprintf("retrieve_password:%s:%s", userID, email),
utils.RandString(6),
time.Minute*5,
)
if loaded {
entry.SetExpiration(time.Now().Add(time.Minute * 5))
}
q := url.Values{}
q.Set("captcha", entry.Value())
q.Set("email", email)
u.RawQuery = q.Encode()
out := bytes.NewBuffer(nil)
err = retrievePasswordTemplate.Execute(out, retrievePasswordPayload{
Captcha: entry.Value(),
Host: host,
URL: u.String(),
Year: time.Now().Year(),
})
if err != nil {
return err
}
return pool.SendEmail(
[]string{email},
"SyncTV Retrieve Password Verification Code",
out.String(),
)
}
func VerifyRetrievePasswordCaptchaEmail(userID, email, captcha string) (bool, error) {
if !EnableEmail.Get() {
return false, ErrEmailNotEnabled
}
if userID == "" {
return false, errors.New("user id is empty")
}
if email == "" {
return false, errors.New("email is empty")
}
if captcha == "" {
return false, errors.New("captcha is empty")
}
if emailCaptcha.CompareValueAndDelete(
fmt.Sprintf("retrieve_password:%s:%s", userID, email),
captcha,
) {
return true, nil
}
return false, nil
}