feat: 添加云端函数HTTP请求、SMTP邮件、数据库操作能力
- 添加HTTP客户端工具支持GET/POST请求 - 添加SMTP邮件发送功能 - 添加数据库操作(db.getRecords, db.deleteRecord, db.deleteRecords) - 添加订单管理云端函数(读取、接收、筛选、成功、失败) - 添加测试推送和邮件通知云端函数 - 修复云端函数编辑页面预览代码换行显示 - 云端变量数据模型重构(合并数据类型)
This commit is contained in:
@@ -457,9 +457,8 @@ type CloudVariable struct {
|
||||
AppID *uint `json:"app_id"`
|
||||
Key string `gorm:"size:100;index" json:"key"`
|
||||
DefaultValue string `gorm:"type:text" json:"default_value"`
|
||||
VarType string `gorm:"size:20;default:string" json:"var_type"` // string, integer, decimal, binary
|
||||
DataType string `gorm:"size:20;default:single" json:"data_type"` // single, stream
|
||||
MaxRecords int `gorm:"default:0" json:"max_records"` // 流水类型最大记录数,0=不限制
|
||||
VarType string `gorm:"size:20;default:string" json:"var_type"` // string, integer, decimal, binary, stream
|
||||
MaxRecords int `gorm:"default:0" json:"max_records"`
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `gorm:"size:100" json:"mime_type"`
|
||||
|
||||
@@ -730,8 +730,8 @@ func handleAppCreateVariableRecord(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
if variable.VarType != "stream" {
|
||||
response.Error(c, 400, "该变量不是记录类型")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -817,8 +817,8 @@ func handleAppGetVariableRecords(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
if variable.VarType != "stream" {
|
||||
response.Error(c, 400, "该变量不是记录类型")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -894,8 +894,8 @@ func handleAppDeleteVariableRecord(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
if variable.VarType != "stream" {
|
||||
response.Error(c, 400, "该变量不是记录类型")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -8,6 +10,8 @@ import (
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/internal/utils/httputil"
|
||||
"verification-platform-backend/internal/utils/smtputil"
|
||||
"verification-platform-backend/pkg/jwt"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
@@ -244,6 +248,68 @@ func handleExecuteDynamicCode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
httpClient := httputil.NewHTTPClient(10 * time.Second)
|
||||
httpObj := map[string]interface{}{
|
||||
"get": func(url string, headers map[string]interface{}) *httputil.HTTPResponse {
|
||||
convertedHeaders := make(map[string]string)
|
||||
for k, v := range headers {
|
||||
convertedHeaders[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
return httpClient.Get(url, convertedHeaders)
|
||||
},
|
||||
"post": func(url string, headers map[string]interface{}, body interface{}) *httputil.HTTPResponse {
|
||||
convertedHeaders := make(map[string]string)
|
||||
for k, v := range headers {
|
||||
convertedHeaders[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
return httpClient.Post(url, convertedHeaders, body)
|
||||
},
|
||||
}
|
||||
if err := vm.Set("http", httpObj); err != nil {
|
||||
response.Error(c, 500, "HTTP对象设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
dbObj := map[string]interface{}{
|
||||
"getRecords": func(variableKey string, page, pageSize int) map[string]interface{} {
|
||||
return getRecords(app.ID, req.UserID, variableKey, page, pageSize)
|
||||
},
|
||||
"deleteRecord": func(variableKey string, recordID uint) map[string]interface{} {
|
||||
return deleteRecord(app.ID, req.UserID, variableKey, recordID)
|
||||
},
|
||||
"deleteRecords": func(variableKey string, recordIDs []uint) map[string]interface{} {
|
||||
return deleteRecords(app.ID, req.UserID, variableKey, recordIDs)
|
||||
},
|
||||
}
|
||||
if err := vm.Set("db", dbObj); err != nil {
|
||||
response.Error(c, 500, "数据库对象设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
smtpObj := map[string]interface{}{
|
||||
"send": func(config map[string]interface{}, to []string, subject, body string, html bool) *smtputil.SMTPResult {
|
||||
smtpConfig := smtputil.SMTPConfig{
|
||||
Host: getString(config, "host"),
|
||||
Port: getInt(config, "port", 587),
|
||||
Username: getString(config, "username"),
|
||||
Password: getString(config, "password"),
|
||||
From: getString(config, "from"),
|
||||
UseTLS: getBool(config, "use_tls", true),
|
||||
}
|
||||
client := smtputil.NewSMTPClient(smtpConfig, 30*time.Second)
|
||||
return client.Send(smtputil.EmailMessage{
|
||||
To: to,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
HTML: html,
|
||||
})
|
||||
},
|
||||
}
|
||||
if err := vm.Set("smtp", smtpObj); err != nil {
|
||||
response.Error(c, 500, "SMTP对象设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
value, err := vm.RunString("(function() { " + dynamicCode.Code + " })()")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "代码执行错误: "+err.Error())
|
||||
@@ -278,6 +344,11 @@ func handleExecuteDynamicCode(c *gin.Context) {
|
||||
response.Error(c, 500, "更新应用变量失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
case "add_record":
|
||||
if err := handleAddRecord(app.ID, req.UserID, actionMap); err != nil {
|
||||
response.Error(c, 500, "添加记录失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,3 +496,213 @@ func toString(v interface{}) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddRecord(appID uint, userID *uint, actionMap map[string]interface{}) error {
|
||||
variableKey, ok := actionMap["variable_key"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("缺少variable_key参数")
|
||||
}
|
||||
|
||||
recordData, ok := actionMap["record_data"]
|
||||
if !ok {
|
||||
return fmt.Errorf("缺少record_data参数")
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
|
||||
return fmt.Errorf("云端变量不存在: %s", variableKey)
|
||||
}
|
||||
|
||||
if variable.VarType != "stream" {
|
||||
return fmt.Errorf("该变量不是记录类型")
|
||||
}
|
||||
|
||||
dataBytes, err := json.Marshal(recordData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化记录数据失败: %v", err)
|
||||
}
|
||||
|
||||
record := model.CloudVariableRecord{
|
||||
CloudVariableID: variable.ID,
|
||||
Data: string(dataBytes),
|
||||
}
|
||||
|
||||
if variable.Scope == "user" && userID != nil {
|
||||
appUserID := *userID
|
||||
record.AppUserID = &appUserID
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&record).Error; err != nil {
|
||||
return fmt.Errorf("创建记录失败: %v", err)
|
||||
}
|
||||
|
||||
if variable.MaxRecords > 0 {
|
||||
var total int64
|
||||
database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID).Count(&total)
|
||||
if int(total) > variable.MaxRecords {
|
||||
deleteCount := int(total) - variable.MaxRecords
|
||||
var oldRecords []model.CloudVariableRecord
|
||||
database.DB.Where("cloud_variable_id = ?", variable.ID).
|
||||
Order("created_at ASC").
|
||||
Limit(deleteCount).
|
||||
Find(&oldRecords)
|
||||
for _, oldRecord := range oldRecords {
|
||||
database.DB.Delete(&oldRecord)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRecords(appID uint, userID *uint, variableKey string, page, pageSize int) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"success": false,
|
||||
"records": []interface{}{},
|
||||
"total": 0,
|
||||
"page": page,
|
||||
"pageSize": pageSize,
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
|
||||
result["error"] = "云端变量不存在"
|
||||
return result
|
||||
}
|
||||
|
||||
if variable.VarType != "stream" {
|
||||
result["error"] = "该变量不是记录类型"
|
||||
return result
|
||||
}
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
var total int64
|
||||
query := database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID)
|
||||
if variable.Scope == "user" && userID != nil {
|
||||
query = query.Where("app_user_id = ?", *userID)
|
||||
}
|
||||
query.Count(&total)
|
||||
|
||||
var records []model.CloudVariableRecord
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
|
||||
result["error"] = "查询记录失败"
|
||||
return result
|
||||
}
|
||||
|
||||
var recordList []interface{}
|
||||
for _, record := range records {
|
||||
var data interface{}
|
||||
if err := json.Unmarshal([]byte(record.Data), &data); err == nil {
|
||||
recordList = append(recordList, map[string]interface{}{
|
||||
"id": record.ID,
|
||||
"data": data,
|
||||
"created_at": record.CreatedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
result["success"] = true
|
||||
result["records"] = recordList
|
||||
result["total"] = total
|
||||
return result
|
||||
}
|
||||
|
||||
func deleteRecord(appID uint, userID *uint, variableKey string, recordID uint) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"success": false,
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
|
||||
result["error"] = "云端变量不存在"
|
||||
return result
|
||||
}
|
||||
|
||||
if variable.VarType != "stream" {
|
||||
result["error"] = "该变量不是记录类型"
|
||||
return result
|
||||
}
|
||||
|
||||
query := database.DB.Where("id = ? AND cloud_variable_id = ?", recordID, variable.ID)
|
||||
if variable.Scope == "user" && userID != nil {
|
||||
query = query.Where("app_user_id = ?", *userID)
|
||||
}
|
||||
|
||||
if err := query.Delete(&model.CloudVariableRecord{}).Error; err != nil {
|
||||
result["error"] = "删除记录失败"
|
||||
return result
|
||||
}
|
||||
|
||||
result["success"] = true
|
||||
return result
|
||||
}
|
||||
|
||||
func deleteRecords(appID uint, userID *uint, variableKey string, recordIDs []uint) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"success": false,
|
||||
"deleted": 0,
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
|
||||
result["error"] = "云端变量不存在"
|
||||
return result
|
||||
}
|
||||
|
||||
if variable.VarType != "stream" {
|
||||
result["error"] = "该变量不是记录类型"
|
||||
return result
|
||||
}
|
||||
|
||||
query := database.DB.Where("id IN ? AND cloud_variable_id = ?", recordIDs, variable.ID)
|
||||
if variable.Scope == "user" && userID != nil {
|
||||
query = query.Where("app_user_id = ?", *userID)
|
||||
}
|
||||
|
||||
deleteResult := query.Delete(&model.CloudVariableRecord{})
|
||||
if deleteResult.Error != nil {
|
||||
result["error"] = "删除记录失败"
|
||||
return result
|
||||
}
|
||||
|
||||
result["success"] = true
|
||||
result["deleted"] = deleteResult.RowsAffected
|
||||
return result
|
||||
}
|
||||
|
||||
func getString(m map[string]interface{}, key string) string {
|
||||
if v, ok := m[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getInt(m map[string]interface{}, key string, defaultValue int) int {
|
||||
if v, ok := m[key]; ok {
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
return val
|
||||
case float64:
|
||||
return int(val)
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getBool(m map[string]interface{}, key string, defaultValue bool) bool {
|
||||
if v, ok := m[key]; ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
@@ -398,7 +398,6 @@ func handleCreateCloudVariable(c *gin.Context) {
|
||||
Key string `json:"key"`
|
||||
DefaultValue string `json:"default_value"`
|
||||
VarType string `json:"var_type"`
|
||||
DataType string `json:"data_type"`
|
||||
MaxRecords int `json:"max_records"`
|
||||
Scope string `json:"scope"`
|
||||
WritePermission string `json:"write_permission"`
|
||||
@@ -431,17 +430,12 @@ func handleCreateCloudVariable(c *gin.Context) {
|
||||
req.VarType = "string"
|
||||
}
|
||||
|
||||
if req.DataType != "single" && req.DataType != "stream" {
|
||||
req.DataType = "single"
|
||||
}
|
||||
|
||||
variable := model.CloudVariable{
|
||||
UserID: userID,
|
||||
AppID: &req.AppID,
|
||||
Key: req.Key,
|
||||
DefaultValue: req.DefaultValue,
|
||||
VarType: req.VarType,
|
||||
DataType: req.DataType,
|
||||
MaxRecords: req.MaxRecords,
|
||||
Scope: req.Scope,
|
||||
WritePermission: req.WritePermission,
|
||||
@@ -464,7 +458,6 @@ func handleUpdateCloudVariable(c *gin.Context) {
|
||||
Key string `json:"key"`
|
||||
DefaultValue string `json:"default_value"`
|
||||
VarType string `json:"var_type"`
|
||||
DataType string `json:"data_type"`
|
||||
MaxRecords int `json:"max_records"`
|
||||
WritePermission string `json:"write_permission"`
|
||||
Description string `json:"description"`
|
||||
@@ -485,9 +478,6 @@ func handleUpdateCloudVariable(c *gin.Context) {
|
||||
variable.Key = req.Key
|
||||
variable.DefaultValue = req.DefaultValue
|
||||
variable.VarType = req.VarType
|
||||
if req.DataType == "single" || req.DataType == "stream" {
|
||||
variable.DataType = req.DataType
|
||||
}
|
||||
variable.MaxRecords = req.MaxRecords
|
||||
if req.WritePermission == "admin" || req.WritePermission == "user" || req.WritePermission == "app_user" {
|
||||
variable.WritePermission = req.WritePermission
|
||||
@@ -715,8 +705,8 @@ func handleGetCloudVariableRecords(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
if variable.VarType != "stream" {
|
||||
response.Error(c, 400, "该变量不是记录类型")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -794,8 +784,8 @@ func handleDeleteCloudVariableRecords(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
if variable.VarType != "stream" {
|
||||
response.Error(c, 400, "该变量不是记录类型")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 10 * time.Second
|
||||
|
||||
type HTTPResponse struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Status string `json:"status"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
JSON map[string]interface{} `json:"json,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPClient struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewHTTPClient(timeout time.Duration) *HTTPClient {
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultTimeout
|
||||
}
|
||||
|
||||
return &HTTPClient{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Get(urlStr string, headers map[string]string) *HTTPResponse {
|
||||
return c.doRequest("GET", urlStr, headers, nil)
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Post(urlStr string, headers map[string]string, body interface{}) *HTTPResponse {
|
||||
return c.doRequest("POST", urlStr, headers, body)
|
||||
}
|
||||
|
||||
func (c *HTTPClient) doRequest(method, urlStr string, headers map[string]string, body interface{}) *HTTPResponse {
|
||||
resp := &HTTPResponse{
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
|
||||
if err := validateURL(urlStr); err != nil {
|
||||
resp.Error = err.Error()
|
||||
return resp
|
||||
}
|
||||
|
||||
var reqBody []byte
|
||||
var err error
|
||||
contentType := "application/json"
|
||||
|
||||
if body != nil {
|
||||
switch v := body.(type) {
|
||||
case string:
|
||||
reqBody = []byte(v)
|
||||
contentType = "text/plain"
|
||||
case map[string]interface{}:
|
||||
reqBody, err = json.Marshal(v)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("failed to marshal body: %v", err)
|
||||
return resp
|
||||
}
|
||||
default:
|
||||
reqBody, err = json.Marshal(v)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("failed to marshal body: %v", err)
|
||||
return resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
if reqBody != nil {
|
||||
req, err = http.NewRequest(method, urlStr, bytes.NewReader(reqBody))
|
||||
} else {
|
||||
req, err = http.NewRequest(method, urlStr, nil)
|
||||
}
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("failed to create request: %v", err)
|
||||
return resp
|
||||
}
|
||||
|
||||
if headers != nil {
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if body != nil && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
httpResp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("request failed: %v", err)
|
||||
return resp
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
resp.StatusCode = httpResp.StatusCode
|
||||
resp.Status = httpResp.Status
|
||||
|
||||
for k, v := range httpResp.Header {
|
||||
if len(v) > 0 {
|
||||
resp.Headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(httpResp.Body); err != nil {
|
||||
resp.Error = fmt.Sprintf("failed to read response body: %v", err)
|
||||
return resp
|
||||
}
|
||||
resp.Body = buf.String()
|
||||
|
||||
if strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") {
|
||||
var jsonData map[string]interface{}
|
||||
if err := json.Unmarshal(buf.Bytes(), &jsonData); err == nil {
|
||||
resp.JSON = jsonData
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func validateURL(urlStr string) error {
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL: %v", err)
|
||||
}
|
||||
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return fmt.Errorf("only http and https protocols are allowed")
|
||||
}
|
||||
|
||||
host := parsedURL.Hostname()
|
||||
if host == "" {
|
||||
return fmt.Errorf("empty host")
|
||||
}
|
||||
|
||||
if isPrivateIP(host) {
|
||||
return fmt.Errorf("access to private IP addresses is not allowed")
|
||||
}
|
||||
|
||||
if isLocalhost(host) {
|
||||
return fmt.Errorf("access to localhost is not allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLocalhost(host string) bool {
|
||||
lowerHost := strings.ToLower(host)
|
||||
return lowerHost == "localhost" || lowerHost == "127.0.0.1" || lowerHost == "::1"
|
||||
}
|
||||
|
||||
func isPrivateIP(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
return true
|
||||
}
|
||||
|
||||
privateBlocks := []string{
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"169.254.0.0/16",
|
||||
"127.0.0.0/8",
|
||||
"::1/128",
|
||||
"fc00::/7",
|
||||
"fe80::/10",
|
||||
}
|
||||
|
||||
for _, block := range privateBlocks {
|
||||
_, cidr, _ := net.ParseCIDR(block)
|
||||
if cidr != nil && cidr.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package smtputil
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SMTPConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
From string
|
||||
UseTLS bool
|
||||
}
|
||||
|
||||
type EmailMessage struct {
|
||||
To []string
|
||||
Subject string
|
||||
Body string
|
||||
HTML bool
|
||||
}
|
||||
|
||||
type SMTPResult struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type SMTPClient struct {
|
||||
config SMTPConfig
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewSMTPClient(config SMTPConfig, timeout time.Duration) *SMTPClient {
|
||||
if timeout <= 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
return &SMTPClient{
|
||||
config: config,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SMTPClient) Send(msg EmailMessage) *SMTPResult {
|
||||
result := &SMTPResult{Success: false}
|
||||
|
||||
if len(msg.To) == 0 {
|
||||
result.Error = "收件人不能为空"
|
||||
return result
|
||||
}
|
||||
|
||||
if c.config.Host == "" {
|
||||
result.Error = "SMTP服务器地址不能为空"
|
||||
return result
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port)
|
||||
|
||||
var auth smtp.Auth
|
||||
if c.config.Username != "" && c.config.Password != "" {
|
||||
auth = smtp.PlainAuth("", c.config.Username, c.config.Password, c.config.Host)
|
||||
}
|
||||
|
||||
from := c.config.From
|
||||
if from == "" {
|
||||
from = c.config.Username
|
||||
}
|
||||
|
||||
contentType := "text/plain"
|
||||
if msg.HTML {
|
||||
contentType = "text/html"
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = from
|
||||
headers["To"] = strings.Join(msg.To, ", ")
|
||||
headers["Subject"] = msg.Subject
|
||||
headers["MIME-Version"] = "1.0"
|
||||
headers["Content-Type"] = fmt.Sprintf("%s; charset=UTF-8", contentType)
|
||||
headers["Date"] = time.Now().Format(time.RFC1123Z)
|
||||
|
||||
var message strings.Builder
|
||||
for k, v := range headers {
|
||||
message.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
|
||||
}
|
||||
message.WriteString("\r\n")
|
||||
message.WriteString(msg.Body)
|
||||
|
||||
if c.config.UseTLS {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: c.config.Host,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("TLS连接失败: %v", err)
|
||||
return result
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, c.config.Host)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("创建SMTP客户端失败: %v", err)
|
||||
return result
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if auth != nil {
|
||||
if err := client.Auth(auth); err != nil {
|
||||
result.Error = fmt.Sprintf("SMTP认证失败: %v", err)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail(from); err != nil {
|
||||
result.Error = fmt.Sprintf("设置发件人失败: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
for _, to := range msg.To {
|
||||
if err := client.Rcpt(to); err != nil {
|
||||
result.Error = fmt.Sprintf("设置收件人失败: %v", err)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("准备邮件数据失败: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(message.String()))
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("写入邮件内容失败: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
result.Error = fmt.Sprintf("关闭邮件写入失败: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
if err := client.Quit(); err != nil {
|
||||
result.Error = fmt.Sprintf("关闭SMTP连接失败: %v", err)
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
err := smtp.SendMail(addr, auth, from, msg.To, []byte(message.String()))
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("发送邮件失败: %v", err)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
result.Success = true
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user