diff --git a/config.default.yaml b/config.default.yaml index 2b94f6f..fd663b3 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -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 diff --git a/config/types.go b/config/types.go index 3ddc4dc..e7ddadd 100644 --- a/config/types.go +++ b/config/types.go @@ -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 { diff --git a/go.mod b/go.mod index cff107a..13ca453 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 46e26e1..3ad9144 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkgs/email/email.go b/pkgs/email/email.go new file mode 100644 index 0000000..010c004 --- /dev/null +++ b/pkgs/email/email.go @@ -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 +} diff --git a/pkgs/email/resend.go b/pkgs/email/resend.go deleted file mode 100644 index d685511..0000000 --- a/pkgs/email/resend.go +++ /dev/null @@ -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 -} diff --git a/service/auth/magic.go b/service/auth/magic.go index f887f0a..f3f47d7 100644 --- a/service/auth/magic.go +++ b/service/auth/magic.go @@ -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", "

Click the link below to verify your email. This link will expire in 10 minutes.

"+url.String()+"",