feat(smtp): add SMTP configuration and management endpoints
- Introduced new endpoints for managing SMTP settings, including GET, PUT, and POST methods for retrieving, updating, and testing SMTP configurations. - Added data structures for SMTP presets and configuration, enhancing the OpenAPI settings to support email functionality. - Organized routing under a new /smtp group for better endpoint management.
This commit is contained in:
parent
c3040559e6
commit
13e16c7099
5 changed files with 1183 additions and 0 deletions
|
|
@ -52,6 +52,12 @@ func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) {
|
|||
search.PUT("/providers/:key/toggle", handleSearchProviderToggle)
|
||||
search.POST("/providers/:key/test", handleSearchProviderTest)
|
||||
search.PUT("/tool-assignment", handleSearchToolAssignment)
|
||||
|
||||
smtpG := group.Group("/smtp")
|
||||
smtpG.GET("", handleSmtpGet)
|
||||
smtpG.PUT("", handleSmtpUpdate)
|
||||
smtpG.PUT("/toggle", handleSmtpToggle)
|
||||
smtpG.POST("/test", handleSmtpTest)
|
||||
}
|
||||
|
||||
// requireOwner checks that the current user is the team owner.
|
||||
|
|
|
|||
586
openapi/setting/smtp.go
Normal file
586
openapi/setting/smtp.go
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/setting"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed smtp_presets.yml
|
||||
var smtpPresetsYML []byte
|
||||
|
||||
const smtpNS = "smtp"
|
||||
|
||||
var smtpPresetsMap map[string][]SmtpPreset
|
||||
|
||||
func init() {
|
||||
smtpPresetsMap = make(map[string][]SmtpPreset)
|
||||
if err := yaml.Unmarshal(smtpPresetsYML, &smtpPresetsMap); err != nil {
|
||||
smtpPresetsMap = map[string][]SmtpPreset{}
|
||||
}
|
||||
}
|
||||
|
||||
func smtpGetPresets(locale string) []SmtpPreset {
|
||||
locale = strings.ToLower(locale)
|
||||
if presets, ok := smtpPresetsMap[locale]; ok {
|
||||
return presets
|
||||
}
|
||||
if presets, ok := smtpPresetsMap["en-us"]; ok {
|
||||
return presets
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpDefaultPreset(presets []SmtpPreset) *SmtpPreset {
|
||||
for i := range presets {
|
||||
if presets[i].Default {
|
||||
return &presets[i]
|
||||
}
|
||||
}
|
||||
if len(presets) > 0 {
|
||||
return &presets[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpScope(info *oauthTypes.AuthorizedInfo) setting.ScopeID {
|
||||
if info.TeamID != "" {
|
||||
return setting.ScopeID{Scope: setting.ScopeTeam, TeamID: info.TeamID}
|
||||
}
|
||||
return setting.ScopeID{Scope: setting.ScopeUser, UserID: info.UserID}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rate limiter: 5 test emails per minute per scope
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
smtpRateMu sync.Mutex
|
||||
smtpRateStore = map[string][]time.Time{}
|
||||
)
|
||||
|
||||
const smtpRateLimit = 5
|
||||
const smtpRateWindow = time.Minute
|
||||
|
||||
func smtpCheckRateLimit(key string) bool {
|
||||
smtpRateMu.Lock()
|
||||
defer smtpRateMu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
cutoff := now.Add(-smtpRateWindow)
|
||||
|
||||
var recent []time.Time
|
||||
for _, t := range smtpRateStore[key] {
|
||||
if t.After(cutoff) {
|
||||
recent = append(recent, t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(recent) >= smtpRateLimit {
|
||||
smtpRateStore[key] = recent
|
||||
return false
|
||||
}
|
||||
|
||||
smtpRateStore[key] = append(recent, now)
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /setting/smtp
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func handleSmtpGet(c *gin.Context) {
|
||||
info := authorized.GetInfo(c)
|
||||
locale := c.Query("locale")
|
||||
if locale == "" {
|
||||
locale = "en-us"
|
||||
}
|
||||
|
||||
presets := smtpGetPresets(locale)
|
||||
|
||||
cfg := SmtpConfig{
|
||||
Enabled: false,
|
||||
PresetKey: "custom",
|
||||
Host: "",
|
||||
Port: 465,
|
||||
Encryption: "ssl",
|
||||
Username: "",
|
||||
Password: "",
|
||||
FromName: "",
|
||||
FromEmail: "",
|
||||
Status: "unconfigured",
|
||||
}
|
||||
|
||||
hasSaved := false
|
||||
if setting.Global != nil {
|
||||
saved, _ := setting.Global.GetMerged(info.UserID, info.TeamID, smtpNS)
|
||||
if saved != nil {
|
||||
smtpLoadConfig(&cfg, saved)
|
||||
hasSaved = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSaved {
|
||||
if def := smtpDefaultPreset(presets); def != nil {
|
||||
cfg.PresetKey = def.Key
|
||||
cfg.Host = def.Host
|
||||
cfg.Port = def.Port
|
||||
cfg.Encryption = def.Encryption
|
||||
}
|
||||
}
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, SmtpPageData{
|
||||
Presets: presets,
|
||||
Config: cfg,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PUT /setting/smtp
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func handleSmtpUpdate(c *gin.Context) {
|
||||
if !guardOwner(c) {
|
||||
return
|
||||
}
|
||||
info := authorized.GetInfo(c)
|
||||
scope := smtpScope(info)
|
||||
|
||||
var body struct {
|
||||
PresetKey string `json:"preset_key"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Encryption string `json:"encryption"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
FromName string `json:"from_name"`
|
||||
FromEmail string `json:"from_email"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if setting.Global == nil {
|
||||
respondError(c, http.StatusInternalServerError, "setting registry not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
existing, _ := setting.Global.Get(scope, smtpNS)
|
||||
|
||||
pwd := body.Password
|
||||
if pwd == "" {
|
||||
if v, ok := existing["password"].(string); ok && v != "" {
|
||||
pwd = cloudDecrypt(v)
|
||||
}
|
||||
}
|
||||
|
||||
validated := false
|
||||
if body.Host != "" && body.Username != "" && pwd != "" {
|
||||
if err := smtpValidateConnection(body.Host, body.Port, body.Encryption, body.Username, pwd); err != nil {
|
||||
respondError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
validated = true
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
for k, v := range existing {
|
||||
m[k] = v
|
||||
}
|
||||
|
||||
m["preset_key"] = body.PresetKey
|
||||
m["host"] = body.Host
|
||||
m["port"] = body.Port
|
||||
m["encryption"] = body.Encryption
|
||||
m["username"] = body.Username
|
||||
m["from_name"] = body.FromName
|
||||
m["from_email"] = body.FromEmail
|
||||
|
||||
if body.Password != "" {
|
||||
m["password"] = cloudEncrypt(body.Password)
|
||||
}
|
||||
|
||||
if validated {
|
||||
m["status"] = "connected"
|
||||
}
|
||||
|
||||
if _, err := setting.Global.Set(scope, smtpNS, m); err != nil {
|
||||
respondError(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cfg := SmtpConfig{
|
||||
PresetKey: "custom",
|
||||
Port: 465,
|
||||
Encryption: "ssl",
|
||||
Status: "unconfigured",
|
||||
}
|
||||
smtpLoadConfig(&cfg, m)
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PUT /setting/smtp/toggle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func handleSmtpToggle(c *gin.Context) {
|
||||
if !guardOwner(c) {
|
||||
return
|
||||
}
|
||||
info := authorized.GetInfo(c)
|
||||
scope := smtpScope(info)
|
||||
|
||||
var body struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if setting.Global == nil {
|
||||
respondError(c, http.StatusInternalServerError, "setting registry not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
existing, _ := setting.Global.Get(scope, smtpNS)
|
||||
m := make(map[string]interface{})
|
||||
for k, v := range existing {
|
||||
m[k] = v
|
||||
}
|
||||
m["enabled"] = body.Enabled
|
||||
if !body.Enabled {
|
||||
m["status"] = "unconfigured"
|
||||
}
|
||||
|
||||
if _, err := setting.Global.Set(scope, smtpNS, m); err != nil {
|
||||
respondError(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cfg := SmtpConfig{
|
||||
PresetKey: "custom",
|
||||
Port: 465,
|
||||
Encryption: "ssl",
|
||||
Status: "unconfigured",
|
||||
}
|
||||
smtpLoadConfig(&cfg, m)
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /setting/smtp/test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func handleSmtpTest(c *gin.Context) {
|
||||
if !guardOwner(c) {
|
||||
return
|
||||
}
|
||||
info := authorized.GetInfo(c)
|
||||
scope := smtpScope(info)
|
||||
|
||||
var body struct {
|
||||
ToEmail string `json:"to_email"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(body.ToEmail) == "" {
|
||||
respondError(c, http.StatusBadRequest, "to_email is required")
|
||||
return
|
||||
}
|
||||
|
||||
rateKey := scope.TeamID
|
||||
if rateKey == "" {
|
||||
rateKey = scope.UserID
|
||||
}
|
||||
if !smtpCheckRateLimit(rateKey) {
|
||||
response.RespondWithSuccess(c, http.StatusOK, SmtpTestResult{
|
||||
Success: false,
|
||||
Message: "Rate limit exceeded, please wait a moment",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if setting.Global == nil {
|
||||
respondError(c, http.StatusInternalServerError, "setting registry not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
saved, _ := setting.Global.Get(scope, smtpNS)
|
||||
if saved == nil {
|
||||
response.RespondWithSuccess(c, http.StatusOK, SmtpTestResult{
|
||||
Success: false,
|
||||
Message: "SMTP not configured",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cfg := SmtpConfig{PresetKey: "custom", Port: 465, Encryption: "ssl", Status: "unconfigured"}
|
||||
smtpLoadConfig(&cfg, saved)
|
||||
|
||||
if cfg.Host == "" || cfg.Username == "" {
|
||||
response.RespondWithSuccess(c, http.StatusOK, SmtpTestResult{
|
||||
Success: false,
|
||||
Message: "SMTP host and username are required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
password := ""
|
||||
if v, ok := saved["password"].(string); ok && v != "" {
|
||||
password = cloudDecrypt(v)
|
||||
}
|
||||
|
||||
fromAddr := cfg.FromEmail
|
||||
if fromAddr == "" {
|
||||
fromAddr = cfg.Username
|
||||
}
|
||||
|
||||
err := smtpSendTestEmail(cfg.Host, cfg.Port, cfg.Encryption, cfg.Username, password, cfg.FromName, fromAddr, body.ToEmail)
|
||||
if err != nil {
|
||||
saved["status"] = "disconnected"
|
||||
setting.Global.Set(scope, smtpNS, saved)
|
||||
response.RespondWithSuccess(c, http.StatusOK, SmtpTestResult{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
saved["status"] = "connected"
|
||||
saved["last_sent_at"] = time.Now().UTC().Format(time.RFC3339)
|
||||
setting.Global.Set(scope, smtpNS, saved)
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, SmtpTestResult{
|
||||
Success: true,
|
||||
Message: "Test email sent successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMTP connection validation (dial + auth, no email)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func smtpValidateConnection(host string, port int, encryption, username, password string) error {
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
auth := smtp.PlainAuth("", username, password, host)
|
||||
|
||||
switch encryption {
|
||||
case "ssl":
|
||||
tlsConfig := &tls.Config{ServerName: host}
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 10 * time.Second}, "tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SSL connection failed: %s", err.Error())
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP client failed: %s", err.Error())
|
||||
}
|
||||
defer client.Quit()
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("authentication failed: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
|
||||
case "tls":
|
||||
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection failed: %s", err.Error())
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP client failed: %s", err.Error())
|
||||
}
|
||||
defer client.Quit()
|
||||
if err = client.StartTLS(&tls.Config{ServerName: host}); err != nil {
|
||||
return fmt.Errorf("STARTTLS failed: %s", err.Error())
|
||||
}
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("authentication failed: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection failed: %s", err.Error())
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP client failed: %s", err.Error())
|
||||
}
|
||||
defer client.Quit()
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("authentication failed: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMTP send helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func smtpSendTestEmail(host string, port int, encryption, username, password, fromName, fromEmail, toEmail string) error {
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
subject := "Yao SMTP Test"
|
||||
body := "This is a test email from Yao to verify your SMTP configuration."
|
||||
|
||||
from := fromEmail
|
||||
if fromName != "" {
|
||||
from = fmt.Sprintf("%s <%s>", fromName, fromEmail)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
|
||||
from, toEmail, subject, body)
|
||||
|
||||
auth := smtp.PlainAuth("", username, password, host)
|
||||
|
||||
switch encryption {
|
||||
case "ssl":
|
||||
return smtpSendSSL(addr, host, auth, fromEmail, toEmail, []byte(msg))
|
||||
case "tls":
|
||||
return smtpSendStartTLS(addr, host, auth, fromEmail, toEmail, []byte(msg))
|
||||
default:
|
||||
return smtp.SendMail(addr, auth, fromEmail, []string{toEmail}, []byte(msg))
|
||||
}
|
||||
}
|
||||
|
||||
func smtpSendSSL(addr, host string, auth smtp.Auth, from, to string, msg []byte) error {
|
||||
tlsConfig := &tls.Config{ServerName: host}
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 10 * time.Second}, "tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SSL connection failed: %s", err.Error())
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP client failed: %s", err.Error())
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("authentication failed: %s", err.Error())
|
||||
}
|
||||
if err = client.Mail(from); err != nil {
|
||||
return fmt.Errorf("MAIL FROM failed: %s", err.Error())
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("RCPT TO failed: %s", err.Error())
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("DATA failed: %s", err.Error())
|
||||
}
|
||||
if _, err = w.Write(msg); err != nil {
|
||||
return fmt.Errorf("write failed: %s", err.Error())
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func smtpSendStartTLS(addr, host string, auth smtp.Auth, from, to string, msg []byte) error {
|
||||
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection failed: %s", err.Error())
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP client failed: %s", err.Error())
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if err = client.StartTLS(&tls.Config{ServerName: host}); err != nil {
|
||||
return fmt.Errorf("STARTTLS failed: %s", err.Error())
|
||||
}
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("authentication failed: %s", err.Error())
|
||||
}
|
||||
if err = client.Mail(from); err != nil {
|
||||
return fmt.Errorf("MAIL FROM failed: %s", err.Error())
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("RCPT TO failed: %s", err.Error())
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("DATA failed: %s", err.Error())
|
||||
}
|
||||
if _, err = w.Write(msg); err != nil {
|
||||
return fmt.Errorf("write failed: %s", err.Error())
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func smtpLoadConfig(cfg *SmtpConfig, m map[string]interface{}) {
|
||||
if v, ok := m["enabled"].(bool); ok {
|
||||
cfg.Enabled = v
|
||||
}
|
||||
if v, ok := m["preset_key"].(string); ok && v != "" {
|
||||
cfg.PresetKey = v
|
||||
}
|
||||
if v, ok := m["host"].(string); ok {
|
||||
cfg.Host = v
|
||||
}
|
||||
if v, ok := m["port"]; ok {
|
||||
switch p := v.(type) {
|
||||
case int:
|
||||
cfg.Port = p
|
||||
case float64:
|
||||
cfg.Port = int(p)
|
||||
case int64:
|
||||
cfg.Port = int(p)
|
||||
}
|
||||
}
|
||||
if v, ok := m["encryption"].(string); ok && v != "" {
|
||||
cfg.Encryption = v
|
||||
}
|
||||
if v, ok := m["username"].(string); ok {
|
||||
cfg.Username = v
|
||||
}
|
||||
if v, ok := m["password"].(string); ok && v != "" {
|
||||
cfg.Password = cloudMaskKey(cloudDecrypt(v))
|
||||
}
|
||||
if v, ok := m["from_name"].(string); ok {
|
||||
cfg.FromName = v
|
||||
}
|
||||
if v, ok := m["from_email"].(string); ok {
|
||||
cfg.FromEmail = v
|
||||
}
|
||||
if v, ok := m["status"].(string); ok && v != "" {
|
||||
cfg.Status = v
|
||||
}
|
||||
if v, ok := m["last_sent_at"].(string); ok && v != "" {
|
||||
cfg.LastSentAt = v
|
||||
}
|
||||
}
|
||||
121
openapi/setting/smtp_presets.yml
Normal file
121
openapi/setting/smtp_presets.yml
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
zh-cn:
|
||||
- key: tencent
|
||||
name: 腾讯企邮
|
||||
host: smtp.exmail.qq.com
|
||||
port: 465
|
||||
encryption: ssl
|
||||
default: true
|
||||
url: https://exmail.qq.com/
|
||||
hint:
|
||||
zh-CN: "密码需使用客户端专用密码,在企业邮设置中生成"
|
||||
en-US: "Use a client-specific password generated in Tencent Exmail settings"
|
||||
|
||||
- key: feishu
|
||||
name: 飞书邮箱
|
||||
host: smtp.feishu.cn
|
||||
port: 465
|
||||
encryption: ssl
|
||||
url: https://www.feishu.cn/
|
||||
hint:
|
||||
zh-CN: "需在飞书管理后台开启邮箱 SMTP 服务"
|
||||
en-US: "Enable SMTP in Feishu admin console"
|
||||
|
||||
- key: aliyun
|
||||
name: 阿里邮箱
|
||||
host: smtp.aliyun.com
|
||||
port: 465
|
||||
encryption: ssl
|
||||
url: https://mail.aliyun.com/
|
||||
hint:
|
||||
zh-CN: "需在阿里邮箱设置中开启 SMTP 服务"
|
||||
en-US: "Enable SMTP in Aliyun Mail settings"
|
||||
|
||||
- key: qq
|
||||
name: QQ邮箱
|
||||
host: smtp.qq.com
|
||||
port: 465
|
||||
encryption: ssl
|
||||
url: https://mail.qq.com/
|
||||
hint:
|
||||
zh-CN: "需在QQ邮箱设置中开启 SMTP 服务并获取授权码"
|
||||
en-US: "Enable SMTP in QQ Mail settings and get authorization code"
|
||||
|
||||
- key: netease163
|
||||
name: 163邮箱
|
||||
host: smtp.163.com
|
||||
port: 465
|
||||
encryption: ssl
|
||||
url: https://mail.163.com/
|
||||
hint:
|
||||
zh-CN: "需在163邮箱设置中开启 SMTP 服务并获取授权码"
|
||||
en-US: "Enable SMTP in 163 Mail settings and get authorization code"
|
||||
|
||||
- key: custom
|
||||
name: 自定义
|
||||
host: ""
|
||||
port: 465
|
||||
encryption: ssl
|
||||
hint:
|
||||
zh-CN: "手动填写 SMTP 服务器信息"
|
||||
en-US: "Manually enter SMTP server details"
|
||||
|
||||
en-us:
|
||||
- key: gmail
|
||||
name: Gmail
|
||||
host: smtp.gmail.com
|
||||
port: 465
|
||||
encryption: ssl
|
||||
default: true
|
||||
url: https://myaccount.google.com/apppasswords
|
||||
hint:
|
||||
zh-CN: "Gmail 需要专用密码(App Password),非登录密码"
|
||||
en-US: "Gmail requires an App Password, not your login password"
|
||||
|
||||
- key: yahoo
|
||||
name: Yahoo Mail
|
||||
host: smtp.mail.yahoo.com
|
||||
port: 465
|
||||
encryption: ssl
|
||||
url: https://login.yahoo.com/account/security
|
||||
hint:
|
||||
zh-CN: "Yahoo 需要应用专用密码"
|
||||
en-US: "Yahoo requires an App Password generated in account security settings"
|
||||
|
||||
- key: sendgrid
|
||||
name: SendGrid
|
||||
host: smtp.sendgrid.net
|
||||
port: 587
|
||||
encryption: tls
|
||||
url: https://app.sendgrid.com/
|
||||
hint:
|
||||
zh-CN: "用户名固定为 apikey,密码填 API Key"
|
||||
en-US: "Username is always \"apikey\", password is your API Key"
|
||||
|
||||
- key: mailgun
|
||||
name: Mailgun
|
||||
host: smtp.mailgun.org
|
||||
port: 587
|
||||
encryption: tls
|
||||
url: https://app.mailgun.com/
|
||||
hint:
|
||||
zh-CN: "在 Mailgun 控制台获取 SMTP 凭证"
|
||||
en-US: "Get SMTP credentials from Mailgun dashboard"
|
||||
|
||||
- key: ses
|
||||
name: Amazon SES
|
||||
host: email-smtp.us-east-1.amazonaws.com
|
||||
port: 587
|
||||
encryption: tls
|
||||
url: https://console.aws.amazon.com/ses/
|
||||
hint:
|
||||
zh-CN: "需在 AWS SES 控制台创建 SMTP 凭证,非 IAM 密钥"
|
||||
en-US: "Create SMTP credentials in AWS SES console, not IAM keys"
|
||||
|
||||
- key: custom
|
||||
name: Custom
|
||||
host: ""
|
||||
port: 465
|
||||
encryption: ssl
|
||||
hint:
|
||||
zh-CN: "手动填写 SMTP 服务器信息"
|
||||
en-US: "Manually enter SMTP server details"
|
||||
|
|
@ -140,3 +140,42 @@ type SearchTestResult struct {
|
|||
Message string `json:"message"`
|
||||
LatencyMs int64 `json:"latency_ms,omitempty"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMTP
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type SmtpPreset struct {
|
||||
Key string `json:"key" yaml:"key"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Port int `json:"port" yaml:"port"`
|
||||
Encryption string `json:"encryption" yaml:"encryption"`
|
||||
Hint map[string]string `json:"hint,omitempty" yaml:"hint"`
|
||||
URL string `json:"url,omitempty" yaml:"url"`
|
||||
Default bool `json:"default,omitempty" yaml:"default"`
|
||||
}
|
||||
|
||||
type SmtpConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PresetKey string `json:"preset_key"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Encryption string `json:"encryption"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
FromName string `json:"from_name"`
|
||||
FromEmail string `json:"from_email"`
|
||||
Status string `json:"status"`
|
||||
LastSentAt string `json:"last_sent_at,omitempty"`
|
||||
}
|
||||
|
||||
type SmtpPageData struct {
|
||||
Presets []SmtpPreset `json:"presets"`
|
||||
Config SmtpConfig `json:"config"`
|
||||
}
|
||||
|
||||
type SmtpTestResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
|
|
|||
431
openapi/tests/setting/smtp_test.go
Normal file
431
openapi/tests/setting/smtp_test.go
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
package setting_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Functional tests (system:root token)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSmtpGet(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
initSettingRegistry(t)
|
||||
token := obtainToken(t, serverURL)
|
||||
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, resp) {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var body map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Contains(t, body, "presets")
|
||||
assert.Contains(t, body, "config")
|
||||
|
||||
presets, ok := body["presets"].([]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 6, len(presets), "should have 6 en-us presets: gmail, yahoo, sendgrid, mailgun, ses, custom")
|
||||
|
||||
config, ok := body["config"].(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, false, config["enabled"])
|
||||
assert.Equal(t, "unconfigured", config["status"])
|
||||
assert.Equal(t, "gmail", config["preset_key"], "default preset for en-us should be gmail")
|
||||
assert.Equal(t, "", config["password"], "password should be empty when unconfigured")
|
||||
}
|
||||
|
||||
func TestSmtpGetZhCN(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
initSettingRegistry(t)
|
||||
token := obtainToken(t, serverURL)
|
||||
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp?locale=zh-cn", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, resp) {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var body map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&body)
|
||||
|
||||
presets, ok := body["presets"].([]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 6, len(presets), "should have 6 zh-cn presets: tencent, feishu, aliyun, qq, netease163, custom")
|
||||
|
||||
first, _ := presets[0].(map[string]interface{})
|
||||
assert.Equal(t, "tencent", first["key"])
|
||||
|
||||
config, _ := body["config"].(map[string]interface{})
|
||||
assert.Equal(t, "tencent", config["preset_key"], "default preset for zh-cn should be tencent")
|
||||
}
|
||||
|
||||
func TestSmtpGetUnauthenticated(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestSmtpUpdate(t *testing.T) {
|
||||
host := os.Getenv("RELIABLE_SMTP_HOST")
|
||||
port := os.Getenv("RELIABLE_SMTP_PORT")
|
||||
user := os.Getenv("RELIABLE_SMTP_USERNAME")
|
||||
pass := os.Getenv("RELIABLE_SMTP_PASSWORD")
|
||||
if host == "" || user == "" || pass == "" {
|
||||
host = os.Getenv("SMTP_HOST")
|
||||
port = os.Getenv("SMTP_PORT")
|
||||
user = os.Getenv("SMTP_USERNAME")
|
||||
pass = os.Getenv("SMTP_PASSWORD")
|
||||
}
|
||||
if host == "" || user == "" || pass == "" {
|
||||
t.Skip("SMTP credentials not set, skipping")
|
||||
}
|
||||
portNum := 465
|
||||
if port != "" {
|
||||
if p, err := strconv.Atoi(port); err == nil {
|
||||
portNum = p
|
||||
}
|
||||
}
|
||||
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
initSettingRegistry(t)
|
||||
token := obtainToken(t, serverURL)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"preset_key": "custom",
|
||||
"host": host,
|
||||
"port": portNum,
|
||||
"encryption": "ssl",
|
||||
"username": user,
|
||||
"password": pass,
|
||||
"from_name": "Test Sender",
|
||||
"from_email": user,
|
||||
}
|
||||
raw, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var body map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&body)
|
||||
assert.Equal(t, host, body["host"])
|
||||
assert.Equal(t, user, body["username"])
|
||||
maskedPwd, _ := body["password"].(string)
|
||||
assert.True(t, strings.Contains(maskedPwd, "..."), "password should be masked: got %s", maskedPwd)
|
||||
|
||||
// GET should also return masked password
|
||||
req2, _ := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp", nil)
|
||||
req2.Header.Set("Authorization", "Bearer "+token)
|
||||
resp2, err := http.DefaultClient.Do(req2)
|
||||
assert.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
|
||||
var getData map[string]interface{}
|
||||
json.NewDecoder(resp2.Body).Decode(&getData)
|
||||
config, _ := getData["config"].(map[string]interface{})
|
||||
getMasked, _ := config["password"].(string)
|
||||
assert.True(t, strings.Contains(getMasked, "..."), "GET should return masked password")
|
||||
}
|
||||
|
||||
func TestSmtpUpdateValidationFailure(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
initSettingRegistry(t)
|
||||
token := obtainToken(t, serverURL)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"preset_key": "gmail",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"encryption": "ssl",
|
||||
"username": "fake@gmail.com",
|
||||
"password": "wrong-password",
|
||||
"from_name": "Test",
|
||||
"from_email": "fake@gmail.com",
|
||||
}
|
||||
raw, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "should reject invalid SMTP credentials")
|
||||
}
|
||||
|
||||
func TestSmtpUpdateKeepPassword(t *testing.T) {
|
||||
host := os.Getenv("RELIABLE_SMTP_HOST")
|
||||
port := os.Getenv("RELIABLE_SMTP_PORT")
|
||||
user := os.Getenv("RELIABLE_SMTP_USERNAME")
|
||||
pass := os.Getenv("RELIABLE_SMTP_PASSWORD")
|
||||
if host == "" || user == "" || pass == "" {
|
||||
host = os.Getenv("SMTP_HOST")
|
||||
port = os.Getenv("SMTP_PORT")
|
||||
user = os.Getenv("SMTP_USERNAME")
|
||||
pass = os.Getenv("SMTP_PASSWORD")
|
||||
}
|
||||
if host == "" || user == "" || pass == "" {
|
||||
t.Skip("SMTP credentials not set, skipping")
|
||||
}
|
||||
portNum := 465
|
||||
if port != "" {
|
||||
if p, err := strconv.Atoi(port); err == nil {
|
||||
portNum = p
|
||||
}
|
||||
}
|
||||
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
initSettingRegistry(t)
|
||||
token := obtainToken(t, serverURL)
|
||||
|
||||
// First save with password
|
||||
payload1 := map[string]interface{}{
|
||||
"preset_key": "custom",
|
||||
"host": host,
|
||||
"port": portNum,
|
||||
"encryption": "ssl",
|
||||
"username": user,
|
||||
"password": pass,
|
||||
"from_name": "Test",
|
||||
"from_email": user,
|
||||
}
|
||||
raw, _ := json.Marshal(payload1)
|
||||
req, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
// Update without password — should keep original and re-validate with existing password
|
||||
payload2 := map[string]interface{}{
|
||||
"preset_key": "custom",
|
||||
"host": host,
|
||||
"port": portNum,
|
||||
"encryption": "ssl",
|
||||
"username": user,
|
||||
"password": "",
|
||||
"from_name": "Updated",
|
||||
"from_email": user,
|
||||
}
|
||||
raw, _ = json.Marshal(payload2)
|
||||
req2, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
|
||||
req2.Header.Set("Authorization", "Bearer "+token)
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
resp2, err := http.DefaultClient.Do(req2)
|
||||
assert.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp2.StatusCode)
|
||||
|
||||
var body map[string]interface{}
|
||||
json.NewDecoder(resp2.Body).Decode(&body)
|
||||
assert.Equal(t, "Updated", body["from_name"])
|
||||
keepMasked, _ := body["password"].(string)
|
||||
assert.True(t, strings.Contains(keepMasked, "..."), "password should be masked (kept original)")
|
||||
}
|
||||
|
||||
func TestSmtpToggle(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
initSettingRegistry(t)
|
||||
token := obtainToken(t, serverURL)
|
||||
|
||||
// Save config first
|
||||
savePayload := map[string]interface{}{
|
||||
"preset_key": "gmail",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"encryption": "ssl",
|
||||
"username": "test@gmail.com",
|
||||
"password": "test-pass",
|
||||
"from_name": "Test",
|
||||
"from_email": "test@gmail.com",
|
||||
}
|
||||
raw, _ := json.Marshal(savePayload)
|
||||
req, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
// Enable
|
||||
enablePayload := map[string]interface{}{"enabled": true}
|
||||
raw, _ = json.Marshal(enablePayload)
|
||||
req2, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp/toggle", bytes.NewReader(raw))
|
||||
req2.Header.Set("Authorization", "Bearer "+token)
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
resp2, err := http.DefaultClient.Do(req2)
|
||||
assert.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp2.StatusCode)
|
||||
|
||||
var body map[string]interface{}
|
||||
json.NewDecoder(resp2.Body).Decode(&body)
|
||||
assert.Equal(t, true, body["enabled"])
|
||||
|
||||
// Disable
|
||||
disablePayload := map[string]interface{}{"enabled": false}
|
||||
raw, _ = json.Marshal(disablePayload)
|
||||
req3, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp/toggle", bytes.NewReader(raw))
|
||||
req3.Header.Set("Authorization", "Bearer "+token)
|
||||
req3.Header.Set("Content-Type", "application/json")
|
||||
resp3, err := http.DefaultClient.Do(req3)
|
||||
assert.NoError(t, err)
|
||||
defer resp3.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp3.StatusCode)
|
||||
|
||||
var body2 map[string]interface{}
|
||||
json.NewDecoder(resp3.Body).Decode(&body2)
|
||||
assert.Equal(t, false, body2["enabled"])
|
||||
assert.Equal(t, "unconfigured", body2["status"])
|
||||
}
|
||||
|
||||
func TestSmtpTest(t *testing.T) {
|
||||
host := os.Getenv("RELIABLE_SMTP_HOST")
|
||||
port := os.Getenv("RELIABLE_SMTP_PORT")
|
||||
user := os.Getenv("RELIABLE_SMTP_USERNAME")
|
||||
pass := os.Getenv("RELIABLE_SMTP_PASSWORD")
|
||||
if host == "" || user == "" || pass == "" {
|
||||
host = os.Getenv("SMTP_HOST")
|
||||
port = os.Getenv("SMTP_PORT")
|
||||
user = os.Getenv("SMTP_USERNAME")
|
||||
pass = os.Getenv("SMTP_PASSWORD")
|
||||
}
|
||||
if host == "" || user == "" || pass == "" {
|
||||
t.Skip("RELIABLE_SMTP_* or SMTP_* env not set, skipping SMTP test")
|
||||
}
|
||||
|
||||
toEmail := os.Getenv("SMTP_TEST_TO")
|
||||
if toEmail == "" {
|
||||
toEmail = user
|
||||
}
|
||||
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
initSettingRegistry(t)
|
||||
token := obtainToken(t, serverURL)
|
||||
|
||||
portNum := 465
|
||||
if port != "" {
|
||||
if p, err := strconv.Atoi(port); err == nil {
|
||||
portNum = p
|
||||
}
|
||||
}
|
||||
|
||||
savePayload := map[string]interface{}{
|
||||
"preset_key": "custom",
|
||||
"host": host,
|
||||
"port": portNum,
|
||||
"encryption": "ssl",
|
||||
"username": user,
|
||||
"password": pass,
|
||||
"from_name": "Yao SMTP Test",
|
||||
"from_email": user,
|
||||
}
|
||||
raw, _ := json.Marshal(savePayload)
|
||||
req, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
testPayload := map[string]interface{}{"to_email": toEmail}
|
||||
raw, _ = json.Marshal(testPayload)
|
||||
req2, err := http.NewRequest("POST", serverURL+baseURL()+"/setting/smtp/test", bytes.NewReader(raw))
|
||||
assert.NoError(t, err)
|
||||
req2.Header.Set("Authorization", "Bearer "+token)
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp2, err := http.DefaultClient.Do(req2)
|
||||
assert.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp2.StatusCode)
|
||||
|
||||
var body map[string]interface{}
|
||||
json.NewDecoder(resp2.Body).Decode(&body)
|
||||
t.Logf("SMTP test result: %+v", body)
|
||||
assert.Equal(t, true, body["success"])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ACL permission tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSmtpACL_ReadOnlyScopeCannotWrite(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
initSettingRegistry(t)
|
||||
|
||||
readToken := obtainRestrictedToken(t, serverURL, "setting:smtp:read:all")
|
||||
|
||||
// GET should work
|
||||
req, _ := http.NewRequest("GET", serverURL+baseURL()+"/setting/smtp", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+readToken)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "read-only scope should allow GET")
|
||||
|
||||
// PUT should be denied
|
||||
payload := map[string]interface{}{
|
||||
"preset_key": "gmail",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"encryption": "ssl",
|
||||
"username": "test@gmail.com",
|
||||
"password": "test-pass",
|
||||
"from_name": "Test",
|
||||
"from_email": "test@gmail.com",
|
||||
}
|
||||
raw, _ := json.Marshal(payload)
|
||||
req2, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/smtp", bytes.NewReader(raw))
|
||||
req2.Header.Set("Authorization", "Bearer "+readToken)
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
resp2, err := http.DefaultClient.Do(req2)
|
||||
assert.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
assert.Equal(t, http.StatusForbidden, resp2.StatusCode, "read-only scope should deny PUT")
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue