mirror of https://github.com/ginuerzh/gost
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
156 lines
3.0 KiB
156 lines
3.0 KiB
package gost
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/go-log/log"
|
|
)
|
|
|
|
// 25 SMTP 服务器发邮件
|
|
// 465 SMTP SSL 客户端发邮件
|
|
// 587 SMTP Submission 客户端发邮件
|
|
// 143 IMAP 收邮件
|
|
// 993 IMAP SSL 收邮件
|
|
// 110 POP3 收邮件
|
|
// 995 POP3 SSL 收邮件
|
|
|
|
var mailPorts = []string{"25", "465", "587", "143", "993", "110", "995", "2525"}
|
|
|
|
type EmailACL struct {
|
|
emails map[string]struct{}
|
|
domains map[string]struct{}
|
|
regex []*regexp.Regexp
|
|
}
|
|
|
|
var emailACL atomic.Value
|
|
|
|
func LoadEmailACL(list []string, regexList []string) error {
|
|
acl := &EmailACL{
|
|
emails: map[string]struct{}{},
|
|
domains: map[string]struct{}{},
|
|
}
|
|
for _, v := range list {
|
|
v = strings.ToLower(strings.TrimSpace(v))
|
|
if strings.HasPrefix(v, "@") {
|
|
domain := strings.TrimPrefix(v, "@")
|
|
acl.domains[domain] = struct{}{}
|
|
} else {
|
|
acl.emails[v] = struct{}{}
|
|
}
|
|
}
|
|
for _, r := range regexList {
|
|
re, err := regexp.Compile(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
acl.regex = append(acl.regex, re)
|
|
}
|
|
emailACL.Store(acl)
|
|
return nil
|
|
}
|
|
|
|
func IsEmailAllowed(email string) bool {
|
|
acl := emailACL.Load().(*EmailACL)
|
|
email = strings.ToLower(strings.TrimSpace(email))
|
|
// 白名单为空 → 拒绝所有
|
|
if len(acl.emails) == 0 && len(acl.domains) == 0 && len(acl.regex) == 0 {
|
|
return false
|
|
}
|
|
// 精确匹配
|
|
if _, ok := acl.emails[email]; ok {
|
|
return true
|
|
}
|
|
// domain 匹配
|
|
parts := strings.Split(email, "@")
|
|
if len(parts) == 2 {
|
|
domain := parts[1]
|
|
if _, ok := acl.domains[domain]; ok {
|
|
return true
|
|
}
|
|
}
|
|
// regex
|
|
for _, r := range acl.regex {
|
|
if r.MatchString(email) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func CheckMailFrom(email string) error {
|
|
if !IsEmailAllowed(email) {
|
|
log.Logf("smtp blocked email: %s", email)
|
|
return fmt.Errorf("550 sender not allowed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckMailTo(address string) error {
|
|
if address == "" {
|
|
return nil
|
|
}
|
|
_, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains(mailPorts, port) {
|
|
return nil
|
|
}
|
|
allowed := false
|
|
for _, h := range config.Auth.EmailWhiteList {
|
|
if strings.EqualFold(h, address) || strings.HasSuffix(address, h) {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
if !allowed {
|
|
// 记录尝试连接非法 SMTP
|
|
fmt.Printf("SMTP access blocked to %s", address)
|
|
return fmt.Errorf("SMTP to this destination is forbidden")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type RateLimit struct {
|
|
count int
|
|
lastTime time.Time
|
|
}
|
|
|
|
var rateLimitMap sync.Map // key: ip/user, value: *RateLimit
|
|
|
|
func CheckRateLimit(ip net.IP, user string, maxPerMinute int) bool {
|
|
|
|
key := ip.String() + ":" + user
|
|
now := time.Now()
|
|
|
|
v, _ := rateLimitMap.LoadOrStore(key, &RateLimit{
|
|
count: 0,
|
|
lastTime: now,
|
|
})
|
|
|
|
rl := v.(*RateLimit)
|
|
|
|
// 超过一分钟窗口 → 重置计数
|
|
if now.Sub(rl.lastTime) > time.Minute {
|
|
rl.count = 0
|
|
rl.lastTime = now
|
|
}
|
|
|
|
if rl.count >= maxPerMinute {
|
|
return false
|
|
}
|
|
|
|
rl.count++
|
|
return true
|
|
}
|
|
|
|
// if !CheckRateLimit(clientIP, username, 50) {
|
|
// return fmt.Errorf("451 Too many messages, rate limit exceeded")
|
|
// }
|
|
|