feature/v2.8.4 (#277)

1、发件列表不展示bug修复
2、smtps支持587端口
3、发件代码优化
This commit is contained in:
Jinnrry
2025-05-10 09:31:23 +08:00
committed by GitHub
parent e194d6ecfd
commit fcc863e8b8
18 changed files with 334 additions and 306 deletions

View File

@@ -34,6 +34,6 @@ COPY --from=serverbuild /work/server/hooks/telegram_push/output/* ./plugins/
COPY --from=serverbuild /work/server/hooks/wechat_push/output/* ./plugins/
COPY --from=serverbuild /work/server/hooks/spam_block/output/* ./plugins/
EXPOSE 25 80 110 443 465 995 993
EXPOSE 25 80 110 443 465 587 995 993
CMD /work/pmail

View File

@@ -28,6 +28,6 @@ COPY --from=serverbuild /work/hooks/telegram_push/output/* ./plugins/
COPY --from=serverbuild /work/hooks/wechat_push/output/* ./plugins/
COPY --from=serverbuild /work/hooks/spam_block/output/* ./plugins/
EXPOSE 25 80 110 443 465 995 993
EXPOSE 25 80 110 443 465 587 995 993
CMD /work/pmail

View File

@@ -57,10 +57,10 @@ First go to [spamhaus](https://check.spamhaus.org/) and check your domain name a
Or
`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 587:587 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
> [!IMPORTANT]
> If your server has a firewall turned on, you need to open ports 25, 80, 110, 443, 465, 993, 995
> If your server has a firewall turned on, you need to open ports 25, 80, 110, 443, 465,587, 993, 995
## 3、Configuration
@@ -102,7 +102,7 @@ POP3 Port: 110/995(SSL)
SMTP Server Address : smtp.[Your Domain]
SMTP Port: 25/465(SSL)
SMTP Port: 25/465、587(SSL)
IMAP Server Address : imap.[Your Domain]

View File

@@ -65,10 +65,10 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
或者
`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 587:587 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
> [!IMPORTANT]
> 如果你服务器开启了防火墙你需要打开25、80、110、443、465、995、993端口
> 如果你服务器开启了防火墙你需要打开25、80、110、443、465、587、995、993端口
## 3、配置
@@ -107,7 +107,7 @@ POP3端口 110/995(SSL)
SMTP地址 smtp.[你的域名]
SMTP端口 25/465(SSL)
SMTP端口 25/465、587(SSL)
IMAP地址 imap.[Your Domain]

View File

@@ -13,14 +13,14 @@
<el-button size="small">
{{ lang.move_btn }}
<el-icon class="el-icon--right">
<EpArrowDownBold />
<EpArrowDownBold/>
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="move(group.id)" v-for="group in groupList" :key="group.id">{{
group.name
}}
<el-dropdown-item @click="move(group.id,group.name)" v-for="group in groupList" :key="group.id">{{
group.name
}}
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -28,8 +28,8 @@
</div>
<div id="table">
<el-table ref="taskTableDataRef" :data="data" :show-header="true" :border="false" @row-click="rowClick"
:row-style="rowStyle">
<el-table-column type="selection" width="30" />
:row-style="rowStyle">
<el-table-column type="selection" width="30"/>
<el-table-column prop="is_read" label="" width="50">
<template #default="scope">
<div>
@@ -64,7 +64,7 @@
<el-table-column prop="title" :label="lang.to" width="150">
<template #default="scope">
<el-tooltip v-for="toInfo in scope.row.to" :key="toInfo" class="box-item" effect="dark"
:content="toInfo.EmailAddress" placement="top">
:content="toInfo.EmailAddress" placement="top">
<el-tag size="small" type="info">{{ toInfo.Name !== '' ? toInfo.Name : toInfo.EmailAddress }}</el-tag>
</el-tooltip>
</template>
@@ -88,7 +88,7 @@
</el-table>
</div>
<div id="pagination">
<el-pagination background layout="prev, pager, next" :page-count="totalPage" @current-change="pageChange" />
<el-pagination background layout="prev, pager, next" :page-count="totalPage" @current-change="pageChange"/>
</div>
</div>
</template>
@@ -96,13 +96,13 @@
<script setup>
import { EpArrowDownBold } from "vue-icons-plus/ep";
import { RouterLink, useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import {EpArrowDownBold} from "vue-icons-plus/ep";
import {RouterLink, useRouter} from 'vue-router'
import {ref, watch} from 'vue'
import useGroupStore from '../stores/group'
import lang from '../i18n/i18n';
import { http } from "@/utils/axios";
import { ElMessage, ElMessageBox } from "element-plus";
import {http} from "@/utils/axios";
import {ElMessage, ElMessageBox} from "element-plus";
const router = useRouter();
@@ -122,7 +122,7 @@ watch(groupStore, async (newV) => {
tag = '{"type":0,"status":-1}'
}
data.value = []
http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
http.post("/api/email/list", {tag: tag, page_size: 10}).then(res => {
data.value = res.data.list
totalPage.value = res.data.total_page
})
@@ -133,7 +133,7 @@ const data = ref([])
const totalPage = ref(0)
const updateList = function () {
http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
http.post("/api/email/list", {tag: tag, page_size: 10}).then(res => {
data.value = res.data.list
totalPage.value = res.data.total_page
})
@@ -163,7 +163,7 @@ const markRead = function () {
confirmButtonText: 'OK',
})
} else {
http.post("/api/email/read", { "ids": ids }).then(res => {
http.post("/api/email/read", {"ids": ids}).then(res => {
if (res.errorNo === 0) {
updateList()
} else {
@@ -177,7 +177,7 @@ const markRead = function () {
}
const move = function (group_id) {
const move = function (group_id, group_name) {
let rows = taskTableDataRef.value?.getSelectionRows()
let ids = []
rows.forEach(element => {
@@ -191,32 +191,32 @@ const move = function (group_id) {
})
} else {
ElMessageBox.confirm(
lang.move_email_confirm,
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
lang.move_email_confirm,
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
)
.then(() => {
http.post("/api/email/move", { "group_id": group_id, "ids": ids }).then(res => {
if (res.errorNo === 0) {
updateList()
ElMessage({
type: 'success',
message: 'Move completed',
})
} else {
ElMessage({
type: 'error',
message: res.errorMsg,
})
}
.then(() => {
http.post("/api/email/move", {"group_id": group_id, "group_name": group_name, "ids": ids}).then(res => {
if (res.errorNo === 0) {
updateList()
ElMessage({
type: 'success',
message: 'Move completed',
})
} else {
ElMessage({
type: 'error',
message: res.errorMsg,
})
}
})
})
})
}
}
@@ -236,42 +236,42 @@ const del = function () {
} else {
ElMessageBox.confirm(
lang.del_email_confirm,
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
lang.del_email_confirm,
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
)
.then(() => {
http.post("/api/email/del", { "ids": ids, "forcedDel": groupTag.status === 3 }).then(res => {
if (res.errorNo === 0) {
updateList()
ElMessage({
type: 'success',
message: 'Delete completed',
})
} else {
ElMessage({
type: 'error',
message: res.errorMsg,
})
}
.then(() => {
http.post("/api/email/del", {"ids": ids, "forcedDel": groupTag.status === 3}).then(res => {
if (res.errorNo === 0) {
updateList()
ElMessage({
type: 'success',
message: 'Delete completed',
})
} else {
ElMessage({
type: 'error',
message: res.errorMsg,
})
}
})
})
})
}
}
const rowStyle = function () {
return { 'cursor': 'pointer' }
return {'cursor': 'pointer'}
}
const pageChange = function (p) {
http.post("/api/email/list", { tag: tag, page_size: 10, current_page: p }).then(res => {
http.post("/api/email/list", {tag: tag, page_size: 10, current_page: p}).then(res => {
data.value = res.data.list
})
}

View File

@@ -3,6 +3,7 @@ package email
import (
"encoding/json"
"github.com/Jinnrry/pmail/dto/response"
"github.com/Jinnrry/pmail/models"
"github.com/Jinnrry/pmail/services/group"
"github.com/Jinnrry/pmail/utils/context"
log "github.com/sirupsen/logrus"
@@ -11,8 +12,9 @@ import (
)
type moveRequest struct {
GroupId int `json:"group_id"`
IDs []int `json:"ids"`
GroupId int `json:"group_id"`
GroupName string `json:"group_name"`
IDs []int `json:"ids"`
}
func Move(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
@@ -31,7 +33,13 @@ func Move(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
return
}
if !group.MoveMailToGroup(ctx, reqData.IDs, reqData.GroupId) {
if name, ok := models.GroupCodeToName[reqData.GroupId]; ok {
err := group.Move2DefaultBox(ctx, reqData.IDs, name)
if err != nil {
response.NewErrorResponse(response.ServerError, "Error", err.Error()).FPrint(w)
return
}
} else if !group.MoveMailToGroup(ctx, reqData.IDs, reqData.GroupId) {
response.NewErrorResponse(response.ServerError, "Error", "").FPrint(w)
return
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/Jinnrry/pmail/dto"
"github.com/Jinnrry/pmail/dto/response"
"github.com/Jinnrry/pmail/i18n"
"github.com/Jinnrry/pmail/models"
"github.com/Jinnrry/pmail/services/group"
"github.com/Jinnrry/pmail/utils/array"
"github.com/Jinnrry/pmail/utils/context"
@@ -14,8 +15,15 @@ import (
)
func GetUserGroupList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
defaultGroup := []*models.Group{
{models.INBOX, i18n.GetText(ctx.Lang, "inbox"), 0, 0, "/"},
{models.Junk, i18n.GetText(ctx.Lang, "junk"), 0, 0, "/"},
{models.Deleted, i18n.GetText(ctx.Lang, "deleted"), 0, 0, "/"},
}
infos := group.GetGroupList(ctx)
response.NewSuccessResponse(infos).FPrint(w)
response.NewSuccessResponse(append(defaultGroup, infos...)).FPrint(w)
}
func GetUserGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {

View File

@@ -63,7 +63,7 @@ type Email struct {
Attachments []*Attachment
ReadReceipt []string
Date string
Status int // 0未发送1已发送2发送失败3删除
Status int // 0未发送1已发送2发送失败3删除5广告邮件
MessageId int64
Size int
}

View File

@@ -10,6 +10,23 @@ import (
"os"
)
func getType(emailId int) int {
var ue models.UserEmail
_, err := db.Instance.Table(&ue).Where("email_id = ?", emailId).Limit(1).Get(&ue)
if err != nil {
fmt.Println(err)
}
if ue.Status == 3 {
return 2
}
if ue.Status == 5 {
return 1
}
return 0
}
func main() {
args := os.Args
@@ -49,7 +66,7 @@ func main() {
if content == "" {
content = tools.Trim(email.Text.String)
}
_, err = file.WriteString(fmt.Sprintf("0 \t%s %s\n", email.Subject, content))
_, err = file.WriteString(fmt.Sprintf("%d \t%s %s\n", getType(email.Id), email.Subject, content))
if err != nil {
fmt.Println(err)
}

View File

@@ -162,7 +162,11 @@ func (s *SpamBlock) ReceiveParseAfter(ctx *context.Context, email *parsemail.Ema
}
if maxClass != 0 && maxScore > s.cfg.Threshold/100 {
email.Status = 5
if maxClass == 2 {
email.Status = 3
} else {
email.Status = 5
}
}
}

View File

@@ -0,0 +1,117 @@
package smtp_server
import (
"database/sql"
"github.com/Jinnrry/pmail/db"
"github.com/Jinnrry/pmail/models"
"github.com/Jinnrry/pmail/utils/context"
"github.com/Jinnrry/pmail/utils/errors"
"github.com/Jinnrry/pmail/utils/id"
"github.com/Jinnrry/pmail/utils/password"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
log "github.com/sirupsen/logrus"
"net"
"strings"
)
// The Backend implements SMTP server methods.
type Backend struct{}
func (bkd *Backend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
remoteAddress := conn.Conn().RemoteAddr()
ctx := &context.Context{}
ctx.SetValue(context.LogID, id.GenLogID())
log.WithContext(ctx).Debugf("新SMTP连接")
return &Session{
RemoteAddress: remoteAddress,
Ctx: ctx,
}, nil
}
// A Session is returned after EHLO.
type Session struct {
RemoteAddress net.Addr
User string
From string
To []string
Ctx *context.Context
}
// AuthMechanisms returns a slice of available auth mechanisms
// supported in this example.
func (s *Session) AuthMechanisms() []string {
return []string{sasl.Plain, sasl.Login}
}
// Auth is the handler for supported authenticators.
func (s *Session) Auth(mech string) (sasl.Server, error) {
log.WithContext(s.Ctx).Debugf("Auth :%s", mech)
if mech == sasl.Plain {
return sasl.NewPlainServer(func(identity, username, password string) error {
return s.AuthPlain(username, password)
}), nil
}
if mech == sasl.Login {
return NewLoginServer(func(username, password string) error {
return s.AuthPlain(username, password)
}), nil
}
return nil, errors.New("Auth Not Supported")
}
func (s *Session) AuthPlain(username, pwd string) error {
log.WithContext(s.Ctx).Debugf("Auth %s %s", username, pwd)
s.User = username
var user models.User
encodePwd := password.Encode(pwd)
infos := strings.Split(username, "@")
if len(infos) > 1 {
username = infos[0]
}
_, err := db.Instance.Where("account =? and password =? and disabled=0", username, encodePwd).Get(&user)
if err != nil && err != sql.ErrNoRows {
log.Errorf("%+v", err)
}
if user.ID > 0 {
s.Ctx.UserAccount = user.Account
s.Ctx.UserID = user.ID
s.Ctx.UserName = user.Name
s.Ctx.IsAdmin = user.IsAdmin == 1
log.WithContext(s.Ctx).Debugf("Auth Success %+v", user)
return nil
}
log.WithContext(s.Ctx).Debugf("登陆错误%s %s", username, pwd)
return errors.New("password error")
}
func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
log.WithContext(s.Ctx).Debugf("Mail Success %+v %+v", from, opts)
s.From = from
return nil
}
func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
log.WithContext(s.Ctx).Debugf("Rcpt Success %+v", to)
s.To = append(s.To, to)
return nil
}
func (s *Session) Reset() {}
func (s *Session) Logout() error {
return nil
}

View File

@@ -2,125 +2,43 @@ package smtp_server
import (
"crypto/tls"
"database/sql"
"github.com/Jinnrry/pmail/config"
"github.com/Jinnrry/pmail/db"
"github.com/Jinnrry/pmail/models"
"github.com/Jinnrry/pmail/utils/context"
"github.com/Jinnrry/pmail/utils/errors"
"github.com/Jinnrry/pmail/utils/id"
"github.com/Jinnrry/pmail/utils/password"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
log "github.com/sirupsen/logrus"
"net"
"strings"
"time"
)
// The Backend implements SMTP server methods.
type Backend struct{}
func (bkd *Backend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
remoteAddress := conn.Conn().RemoteAddr()
ctx := &context.Context{}
ctx.SetValue(context.LogID, id.GenLogID())
log.WithContext(ctx).Debugf("新SMTP连接")
return &Session{
RemoteAddress: remoteAddress,
Ctx: ctx,
}, nil
}
// A Session is returned after EHLO.
type Session struct {
RemoteAddress net.Addr
User string
From string
To []string
Ctx *context.Context
}
// AuthMechanisms returns a slice of available auth mechanisms
// supported in this example.
func (s *Session) AuthMechanisms() []string {
return []string{sasl.Plain, sasl.Login}
}
// Auth is the handler for supported authenticators.
func (s *Session) Auth(mech string) (sasl.Server, error) {
log.WithContext(s.Ctx).Debugf("Auth :%s", mech)
if mech == sasl.Plain {
return sasl.NewPlainServer(func(identity, username, password string) error {
return s.AuthPlain(username, password)
}), nil
}
if mech == sasl.Login {
return NewLoginServer(func(username, password string) error {
return s.AuthPlain(username, password)
}), nil
}
return nil, errors.New("Auth Not Supported")
}
func (s *Session) AuthPlain(username, pwd string) error {
log.WithContext(s.Ctx).Debugf("Auth %s %s", username, pwd)
s.User = username
var user models.User
encodePwd := password.Encode(pwd)
infos := strings.Split(username, "@")
if len(infos) > 1 {
username = infos[0]
}
_, err := db.Instance.Where("account =? and password =? and disabled=0", username, encodePwd).Get(&user)
if err != nil && err != sql.ErrNoRows {
log.Errorf("%+v", err)
}
if user.ID > 0 {
s.Ctx.UserAccount = user.Account
s.Ctx.UserID = user.ID
s.Ctx.UserName = user.Name
s.Ctx.IsAdmin = user.IsAdmin == 1
log.WithContext(s.Ctx).Debugf("Auth Success %+v", user)
return nil
}
log.WithContext(s.Ctx).Debugf("登陆错误%s %s", username, pwd)
return errors.New("password error")
}
func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
log.WithContext(s.Ctx).Debugf("Mail Success %+v %+v", from, opts)
s.From = from
return nil
}
func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
log.WithContext(s.Ctx).Debugf("Rcpt Success %+v", to)
s.To = append(s.To, to)
return nil
}
func (s *Session) Reset() {}
func (s *Session) Logout() error {
return nil
}
var instance *smtp.Server
var instanceTls *smtp.Server
var instanceTlsNew *smtp.Server
func StartWithTLSNew() {
be := &Backend{}
instanceTlsNew = smtp.NewServer(be)
instanceTlsNew.Addr = ":587"
instanceTlsNew.Domain = config.Instance.Domain
instanceTlsNew.ReadTimeout = 10 * time.Second
instanceTlsNew.WriteTimeout = 10 * time.Second
instanceTlsNew.MaxMessageBytes = 1024 * 1024 * 30
instanceTlsNew.MaxRecipients = 50
// force TLS for auth
instanceTlsNew.AllowInsecureAuth = true
// Load the certificate and key
cer, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
if err != nil {
log.Fatal(err)
return
}
// Configure the TLS support
instanceTlsNew.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
log.Println("Starting Smtp With SSL Server Port:", instanceTlsNew.Addr)
if err := instanceTlsNew.ListenAndServeTLS(); err != nil {
log.Fatal(err)
}
}
func StartWithTLS() {
be := &Backend{}
@@ -185,4 +103,8 @@ func Stop() {
if instanceTls != nil {
instanceTls.Close()
}
if instanceTlsNew != nil {
instanceTlsNew.Close()
}
}

View File

@@ -200,8 +200,8 @@ func testCreateGroup(t *testing.T) {
t.Error("CreateGroup Api Error!", data)
}
dt := data.Data.([]any)
if len(dt) != 1 {
t.Error("Group List Is Empty!")
if len(dt) != 4 {
t.Errorf("Group List Check Error!,response: %+v", data)
}
}

View File

@@ -8,7 +8,7 @@ type UserEmail struct {
EmailID int `xorm:"email_id not null index('idx_eid') index comment('信件id')"`
IsRead int8 `xorm:"is_read tinyint(1) comment('是否已读')" json:"is_read"`
GroupId int `xorm:"group_id int notnull default(0) comment('分组id')'" json:"group_id"`
Status int8 `xorm:"status tinyint(4) notnull default(0) comment('0未发送或收件1已发送2发送失败3删除')" json:"status"` // 0未发送或收件1已发送2发送失败 3删除 4草稿箱(Drafts) 5骚扰邮件(Junk)
Status int8 `xorm:"status tinyint(4) notnull default(0) comment('0未发送或收件1已发送2发送失败3删除 4草稿 5广告')" json:"status"` // 0未发送或收件1已发送2发送失败 3删除 4草稿箱(Drafts) 5骚扰邮件(Junk)
Created time.Time `xorm:"create datetime created index('idx_create_time')"`
}

View File

@@ -43,6 +43,7 @@ func Init(serverVersion string) {
// smtp server start
go smtp_server.Start()
go smtp_server.StartWithTLS()
go smtp_server.StartWithTLSNew()
// http server start
go http_server.HttpsStart()
go http_server.HttpStart()

View File

@@ -51,7 +51,9 @@ func genSQL(ctx *context.Context, count bool, tagInfo dto.SearchTag, keyword str
sql += " and ue.status =? "
sqlParams = append(sqlParams, tagInfo.Status)
} else if tagInfo.Status == -1 {
sql += " and ue.status = 0"
if tagInfo.Type != 1 {
sql += " and ue.status = 0"
}
}
if tagInfo.Type != -1 {

View File

@@ -4,7 +4,6 @@ import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"github.com/Jinnrry/pmail/config"
"github.com/Jinnrry/pmail/dto/parsemail"
"github.com/Jinnrry/pmail/models"
@@ -28,7 +27,6 @@ type mxDomain struct {
func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string, user *models.User) error {
log.WithContext(ctx).Debugf("开始转发邮件")
sender := fmt.Sprintf("%s@%s", user.Account, config.Instance.Domains[0])
b := e.ForwardBuildBytes(ctx, user)
@@ -39,91 +37,9 @@ func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string, us
{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
}
}
err, _ := doSend(ctx, config.Instance.Domains[0], b, to, e.From.EmailAddress)
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, sender, config.Instance.Domains[0], 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, sender, config.Instance.Domains[0], 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, sender, config.Instance.Domains[0], buildAddress(tos), b)
}
}
// 证书错误,从新选取证书发送
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
// 单测使用
if domain.domain == "localhost" {
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, sender, config.Instance.Domains[0], 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, sender, config.Instance.Domains[0], 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
return err
}
func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
@@ -135,6 +51,12 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
var to []*parsemail.User
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
return doSend(ctx, fromDomain, b, to, e.From.EmailAddress)
}
func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemail.User, from string) (error, map[string]error) {
// 按域名整理
toByDomain := map[mxDomain][]*parsemail.User{}
for _, s := range to {
@@ -181,32 +103,56 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
tos := tos
as.WaitProcess(func(p any) {
err := smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
if domain.domain == "localhost" {
err := smtp.SendMailUnsafe("", domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
if err != nil {
log.WithContext(ctx).Errorf("send error %s", err.Error())
}
return
}
// 使用其他方式发送
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, fromDomain, 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, fromDomain, buildAddress(tos), b)
// 优先尝试25端口starttls方式投递
err := smtp.SendMail("", domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
if err == nil {
return
}
// 证书错误,从新选取证书发送
var certificateErr *tls.CertificateVerificationError
if errors.As(err, &certificateErr) {
// 单测使用
var hostnameErr x509.HostnameError
if errors.As(certificateErr.Err, &hostnameErr) {
if hostnameErr.Certificate != nil {
certificateHostName := hostnameErr.Certificate.DNSNames
// 重新选取证书发送
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
}
}
}
if err == nil {
return
}
log.WithContext(ctx).Infof("SMTP STARTTLS on 25 Send Error. %s", err.Error())
// 证书错误,从新选取证书发送
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
// 单测使用
if domain.domain == "localhost" {
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, 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, fromDomain, buildAddress(tos), b)
}
}
}
// 再试用587投递
err = smtp.SendMailWithTls("", domain.mxHost+":587", nil, from, fromDomain, buildAddress(tos), data)
if err == nil {
return
}
log.WithContext(ctx).Infof("SMTPS on 587 Send Error. %s", err.Error())
// 再次尝试465投递
err = smtp.SendMailWithTls("", domain.mxHost+":465", nil, from, fromDomain, buildAddress(tos), data)
if err == nil {
return
}
log.WithContext(ctx).Infof("SMTPS on 465 Send Error. %s", err.Error())
// 最后尝试非安全方式投递
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
if err == nil {
log.WithContext(ctx).Warnf("Send By Unsafe SMTP")
return
}
if err != nil {
@@ -235,7 +181,6 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ",")), orgMap
}
return nil, orgMap
}
func buildAddress(u []*parsemail.User) []string {

View File

@@ -69,6 +69,10 @@ func Dial(addr, fromDomain string) (*Client, error) {
// with tls
func DialTls(addr, domain, fromDomain string) (*Client, error) {
if domain == "" {
domain = fromDomain
}
// TLS config
tlsconfig := &tls.Config{
InsecureSkipVerify: true,