mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-10-28 08:35:33 +08:00
* feat(email): add Resend delivery provider Add Resend as an alternative to SMTP with separately editable sender address and name, credential-safe configuration status, and validation before activation. Preserve the target repository's email notification and balance reminder settings. Only Resend changes are transplanted from hicoderai/new-api; no landing, legal-document, model-group, pricing, branding, or dependency changes are included. Validation: Go common/controller email regression tests; 19 frontend email and notification tests; frontend production build; tsgo typecheck; touched-file lint. Frontend validation uses the target lockfile's Axios 1.18.1 with local cached tools. Security references: OWASP Authentication, Session Management, and Forgot Password Cheat Sheets. Tests cover credential non-disclosure, invalid activation, sanitized transport errors, and no automatic retry or fallback; token and session flows are unchanged. https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html * fix(email): log Resend rejections and translate Resend errors Resend's reason for a rejected or failed send now goes to the server log. People who asked for the email still see the short error. Resend and email method errors are translatable messages in all seven languages. The email settings hints are shorter and reuse the existing key hint. The new frontend files carry the license header, the sender name check passes lint, and test fixtures use neutral names. * fix(email): check Resend settings while Resend is active Saving the sender address or API key now checks the whole Resend setup while Resend sends the mail. A typo the form accepts, such as notify..team@example.com, is rejected instead of breaking every send. Resend's error text and network errors are quoted in the server log. --------- Co-authored-by: CaIon <i@caion.me>
151 lines
3.7 KiB
Go
151 lines
3.7 KiB
Go
package common
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/smtp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func generateMessageID() (string, error) {
|
|
split := strings.Split(SMTPFrom, "@")
|
|
if len(split) < 2 {
|
|
return "", fmt.Errorf("invalid SMTP account")
|
|
}
|
|
domain := strings.Split(SMTPFrom, "@")[1]
|
|
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil
|
|
}
|
|
|
|
func shouldUseSMTPLoginAuth() bool {
|
|
if SMTPForceAuthLogin {
|
|
return true
|
|
}
|
|
return isOutlookServer(SMTPAccount) || slices.Contains(EmailLoginAuthServerList, SMTPServer)
|
|
}
|
|
|
|
func getSMTPAuth() smtp.Auth {
|
|
return AutoSMTPAuth(SMTPAccount, SMTPToken)
|
|
}
|
|
|
|
func shouldAuthenticateSMTP() bool {
|
|
return SMTPAccount != "" && SMTPToken != ""
|
|
}
|
|
|
|
func smtpTLSConfig() *tls.Config {
|
|
return &tls.Config{
|
|
ServerName: SMTPServer,
|
|
InsecureSkipVerify: SMTPInsecureSkipVerify, // #nosec G402 -- admin-controlled SMTP compatibility option.
|
|
}
|
|
}
|
|
|
|
func newSMTPClient(addr string) (*smtp.Client, error) {
|
|
if SMTPSSLEnabled || (SMTPPort == 465 && !SMTPStartTLSEnabled) {
|
|
conn, err := tls.Dial("tcp", addr, smtpTLSConfig())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client, err := smtp.NewClient(conn, SMTPServer)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
client, err := smtp.Dial(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if SMTPStartTLSEnabled {
|
|
startTLSSupported, _ := client.Extension("STARTTLS")
|
|
if !startTLSSupported {
|
|
_ = client.Close()
|
|
return nil, fmt.Errorf("SMTP server does not support STARTTLS")
|
|
}
|
|
if err := client.StartTLS(smtpTLSConfig()); err != nil {
|
|
_ = client.Close()
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func SendEmail(subject string, receiver string, content string) error {
|
|
OptionMapRWMutex.RLock()
|
|
provider := OptionMap["EmailProvider"]
|
|
resendKey := OptionMap["ResendApiKey"]
|
|
resendFrom := OptionMap["ResendFrom"]
|
|
OptionMapRWMutex.RUnlock()
|
|
// One selector controls delivery. Never fall back to a second provider:
|
|
// a failed response does not prove that the first provider did not send.
|
|
switch provider {
|
|
case "resend":
|
|
return sendResendEmail(resendKey, resendFrom, subject, receiver, content)
|
|
case "", "smtp": // Preserve existing installations without a selector.
|
|
default:
|
|
return NewMessage("Email sending method must be SMTP or Resend")
|
|
}
|
|
if SMTPFrom == "" { // for compatibility
|
|
SMTPFrom = SMTPAccount
|
|
}
|
|
id, err2 := generateMessageID()
|
|
if err2 != nil {
|
|
return err2
|
|
}
|
|
if SMTPServer == "" && SMTPAccount == "" {
|
|
return NewMessage("SMTP server is not configured")
|
|
}
|
|
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
|
|
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
|
"From: %s <%s>\r\n"+
|
|
"Subject: %s\r\n"+
|
|
"Date: %s\r\n"+
|
|
"Message-ID: %s\r\n"+ // 添加 Message-ID 头
|
|
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
|
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), id, content))
|
|
auth := getSMTPAuth()
|
|
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
|
to := strings.Split(receiver, ";")
|
|
var err error
|
|
client, err := newSMTPClient(addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
if shouldAuthenticateSMTP() {
|
|
if err = client.Auth(auth); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err = client.Mail(SMTPFrom); err != nil {
|
|
return err
|
|
}
|
|
for _, receiver := range to {
|
|
if err = client.Rcpt(receiver); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
w, err := client.Data()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = w.Write(mail)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = w.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = client.Quit()
|
|
if err != nil {
|
|
SysError(LogText("failed to send email to %s: %v", receiver, err))
|
|
}
|
|
return err
|
|
}
|