Change resend to using smtp
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished

Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
2026-01-02 16:26:21 +08:00
parent a98ab26fa4
commit d70ade4907
7 changed files with 91 additions and 95 deletions

View File

@@ -20,7 +20,12 @@ search:
host: 127.0.0.1
api_key: ""
email:
resend_api_key: abc
host:
port:
username:
password:
security:
insecure_skip_verify:
from:
secrets:
jwt_secret: example

View File

@@ -40,8 +40,13 @@ type search struct {
}
type email struct {
ResendApiKey string `yaml:"resend_api_key"`
From string `yaml:"from"`
Host string `yaml:"host"`
Port string `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Security string `yaml:"security"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
From string `yaml:"from"`
}
type secrets struct {

2
go.mod
View File

@@ -64,6 +64,8 @@ require (
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gorm.io/datatypes v1.2.7 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
gorm.io/driver/postgres v1.6.0 // indirect

4
go.sum
View File

@@ -139,7 +139,11 @@ golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=

70
pkgs/email/email.go Normal file
View File

@@ -0,0 +1,70 @@
package email
import (
"crypto/tls"
"errors"
"strings"
"time"
"github.com/spf13/viper"
gomail "gopkg.in/gomail.v2"
)
type Client struct {
dialer *gomail.Dialer
from string
}
func NewSMTPClient() (*Client, error) {
host := viper.GetString("email.host")
port := viper.GetInt("email.port")
user := viper.GetString("email.username")
pass := viper.GetString("email.password")
from := viper.GetString("email.from")
security := strings.ToLower(viper.GetString("email.security"))
insecure := viper.GetBool("email.insecure_skip_verify")
if host == "" || port == 0 || user == "" || pass == "" {
return nil, errors.New("SMTP config not set")
}
dialer := gomail.NewDialer(host, port, user, pass)
dialer.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: insecure,
}
switch security {
case "ssl":
dialer.SSL = true
case "starttls":
dialer.SSL = false
case "plain", "":
dialer.SSL = false
dialer.TLSConfig = nil
default:
return nil, errors.New("unknown smtp security mode: " + security)
}
return &Client{
dialer: dialer,
from: from,
}, nil
}
func (c *Client) Send(to, subject, html string) (string, error) {
m := gomail.NewMessage()
m.SetHeader("From", c.from)
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/html", html)
if err := c.dialer.DialAndSend(m); err != nil {
return "", err
}
return time.Now().Format(time.RFC3339Nano), nil
}

View File

@@ -1,87 +0,0 @@
package email
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/spf13/viper"
)
type Client struct {
apiKey string
http *http.Client
}
// Resend service client
func NewResendClient() (*Client, error) {
key := viper.GetString("email.resend_api_key")
if key == "" {
return nil, errors.New("RESEND_API_KEY not set")
}
return &Client{
apiKey: key,
http: &http.Client{
Timeout: 10 * time.Second,
},
}, nil
}
type sendEmailRequest struct {
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
HTML string `json:"html,omitempty"`
Text string `json:"text,omitempty"`
}
type sendEmailResponse struct {
ID string `json:"id"`
}
// Send email by resend API
func (c *Client) Send(to, subject, html string) (string, error) {
reqBody := sendEmailRequest{
From: viper.GetString("email.from"),
To: []string{to},
Subject: subject,
HTML: html,
}
body, err := json.Marshal(reqBody)
if err != nil {
return "", err
}
req, err := http.NewRequest(
http.MethodPost,
"https://api.resend.com/emails",
bytes.NewReader(body),
)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return "", errors.New("resend send failed")
}
var res sendEmailResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return "", err
}
return res.ID, nil
}

View File

@@ -6,8 +6,6 @@ import (
"nixcn-cms/pkgs/email"
"nixcn-cms/pkgs/turnstile"
log "github.com/sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
@@ -59,13 +57,12 @@ func Magic(c *gin.Context) {
return
} else {
// Send email using resend
resend, err := email.NewResendClient()
emailClient, err := email.NewSMTPClient()
if err != nil {
log.Error(err)
c.JSON(500, gin.H{"status": "invalid email config"})
return
}
resend.Send(
emailClient.Send(
req.Email,
"NixCN CMS Email Verify",
"<p>Click the link below to verify your email. This link will expire in 10 minutes.</p><a href="+url.String()+">"+url.String()+"</a>",