Smail/server/utils/send/send.go
Jinnrry 4c22035819
V2.5.2 (#145)
* 移除自动DNS记录验证方式

* v2.5.2 (#13)

1、重写SSL证书申请逻辑,支持DNS记录验证方式
2、修复无法查看详情的bug
3、优化手动设置证书的逻辑
4、发送失败邮件添加错误提醒
5、补充详情接口的单元测试

Co-authored-by: jinnrry <jinnrry@users.noreply.github.com>
Reviewed-on: http://192.168.123.5/jinnrry/PMail_Github/pulls/13

---------

Co-authored-by: jinnrry <jinnrry@users.noreply.github.com>
Co-authored-by: jinnrry <i@jinnrry.com>
2024-07-06 12:54:58 +08:00

300 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package send
import (
"crypto/tls"
"crypto/x509"
"errors"
log "github.com/sirupsen/logrus"
"net"
"pmail/dto/parsemail"
"pmail/utils/array"
"pmail/utils/async"
"pmail/utils/consts"
"pmail/utils/context"
"pmail/utils/smtp"
"strings"
"sync"
)
type mxDomain struct {
domain string
mxHost string
}
// Forward 转发邮件
func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string) error {
log.WithContext(ctx).Debugf("开始转发邮件")
b := e.ForwardBuildBytes(ctx, forwardAddress)
var to []*parsemail.User
to = []*parsemail.User{
{EmailAddress: forwardAddress},
}
// 按域名整理
toByDomain := map[mxDomain][]*parsemail.User{}
for _, s := range to {
args := strings.Split(s.EmailAddress, "@")
if len(args) == 2 {
if args[1] == consts.TEST_DOMAIN {
// 测试使用
address := mxDomain{
domain: "localhost",
mxHost: "127.0.0.1",
}
toByDomain[address] = append(toByDomain[address], s)
} else {
//查询dns mx记录
mxInfo, err := net.LookupMX(args[1])
address := mxDomain{
domain: "smtp." + args[1],
mxHost: "smtp." + args[1],
}
if err != nil {
log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
}
if len(mxInfo) > 0 {
address = mxDomain{
domain: args[1],
mxHost: mxInfo[0].Host,
}
}
toByDomain[address] = append(toByDomain[address], s)
}
} else {
log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
continue
}
}
var errEmailAddress []string
as := async.New(ctx)
for domain, tos := range toByDomain {
domain := domain
tos := tos
as.WaitProcess(func(p any) {
err := smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
// 使用其他方式发送
if err != nil {
// EOF 表示未知错误,此时降级为非tls连接发送(目前仅139邮箱有这个问题)
if errors.Is(err, smtp.NoSupportSTARTTLSError) || err.Error() == "EOF" {
err = smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
if err != nil {
log.WithContext(ctx).Warnf("Unsafe! %s Server Not Support SMTPS & STARTTLS", domain.domain)
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
}
}
// 证书错误,从新选取证书发送
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
// 单测使用
if domain.domain == "localhost" {
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
} else if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
if hostnameErr.Certificate != nil {
certificateHostName := hostnameErr.Certificate.DNSNames
// 重新选取证书发送
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
}
}
}
}
if err != nil {
log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
for _, user := range tos {
errEmailAddress = append(errEmailAddress, user.EmailAddress)
}
}
}, nil)
}
as.Wait()
if len(errEmailAddress) > 0 {
return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ","))
}
return nil
}
func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
b := e.BuildBytes(ctx, true)
log.WithContext(ctx).Debugf("Message Infos : %s", string(b))
var to []*parsemail.User
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
// 按域名整理
toByDomain := map[mxDomain][]*parsemail.User{}
for _, s := range to {
args := strings.Split(s.EmailAddress, "@")
if len(args) == 2 {
if args[1] == consts.TEST_DOMAIN {
// 测试使用
address := mxDomain{
domain: "localhost",
mxHost: "127.0.0.1",
}
toByDomain[address] = append(toByDomain[address], s)
} else {
//查询dns mx记录
mxInfo, err := net.LookupMX(args[1])
address := mxDomain{
domain: "smtp." + args[1],
mxHost: "smtp." + args[1],
}
if err != nil {
log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
}
if len(mxInfo) > 0 {
address = mxDomain{
domain: args[1],
mxHost: mxInfo[0].Host,
}
}
toByDomain[address] = append(toByDomain[address], s)
}
} else {
log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
continue
}
}
var errEmailAddress []string
errMap := sync.Map{}
as := async.New(ctx)
for domain, tos := range toByDomain {
domain := domain
tos := tos
as.WaitProcess(func(p any) {
err := smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
// 使用其他方式发送
if err != nil {
// EOF 表示未知错误,此时降级为非tls连接发送(目前仅139邮箱有这个问题)
if errors.Is(err, smtp.NoSupportSTARTTLSError) || err.Error() == "EOF" {
err = smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
if err != nil {
log.WithContext(ctx).Warnf("Unsafe! %s Server Not Support SMTPS & STARTTLS", domain.domain)
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
}
}
// 证书错误,从新选取证书发送
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
// 单测使用
if domain.domain == "localhost" {
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
} else if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
if hostnameErr.Certificate != nil {
certificateHostName := hostnameErr.Certificate.DNSNames
// 重新选取证书发送
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
}
}
}
}
if err != nil {
log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
for _, user := range tos {
errEmailAddress = append(errEmailAddress, user.EmailAddress)
}
}
errMap.Store(domain.domain, err)
}, nil)
}
as.Wait()
orgMap := map[string]error{}
errMap.Range(func(key, value any) bool {
if value != nil {
orgMap[key.(string)] = value.(error)
} else {
orgMap[key.(string)] = nil
}
return true
})
if len(errEmailAddress) > 0 {
return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ",")), orgMap
}
return nil, orgMap
}
func buildAddress(u []*parsemail.User) []string {
var ret []string
for _, user := range u {
ret = append(ret, user.EmailAddress)
}
return ret
}
func domainMatch(domain string, dnsNames []string) string {
if len(dnsNames) == 0 {
return domain
}
secondMatch := ""
for _, name := range dnsNames {
if strings.Contains(name, "smtp") {
secondMatch = name
}
if name == domain {
return name
}
if strings.Contains(name, "*") {
nameArg := strings.Split(name, ".")
domainArg := strings.Split(domain, ".")
match := true
for i := 0; i < len(nameArg); i++ {
if nameArg[len(nameArg)-1-i] == "*" {
continue
}
if len(domainArg) > i {
if nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
continue
}
}
match = false
break
}
for i := 0; i < len(domainArg); i++ {
if len(nameArg) > i && nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
continue
}
if len(nameArg) > i && nameArg[len(nameArg)-1-i] == "*" {
continue
}
match = false
break
}
if match {
return domain
}
}
}
if secondMatch != "" {
return strings.ReplaceAll(secondMatch, "*.", "")
}
return strings.ReplaceAll(dnsNames[0], "*.", "")
}