Просмотр исходного кода

feature/v2.8.4 (#277)

1、发件列表不展示bug修复
2、smtps支持587端口
3、发件代码优化
Jinnrry 11 месяцев назад
Родитель
Сommit
fcc863e8b8

+ 1 - 1
Dockerfile

@@ -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

+ 1 - 1
DockerfileGithubAction

@@ -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

+ 3 - 3
README.md

@@ -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]
 

+ 3 - 3
README_CN.md

@@ -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]
 

+ 65 - 65
fe/src/views/ListView.vue

@@ -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
   })
 }

+ 11 - 3
server/controllers/email/move.go

@@ -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
 	}

+ 9 - 1
server/controllers/group.go

@@ -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) {

+ 1 - 1
server/dto/parsemail/email.go

@@ -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
 }

+ 18 - 1
server/hooks/spam_block/export/export.go

@@ -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)
 			}

+ 5 - 1
server/hooks/spam_block/spam_block.go

@@ -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
+		}
 	}
 }
 

+ 117 - 0
server/listen/smtp_server/action.go

@@ -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
+}

+ 28 - 106
server/listen/smtp_server/smtp.go

@@ -2,126 +2,44 @@ 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
+var instance *smtp.Server
+var instanceTls *smtp.Server
+var instanceTlsNew *smtp.Server
 
-	encodePwd := password.Encode(pwd)
+func StartWithTLSNew() {
+	be := &Backend{}
 
-	infos := strings.Split(username, "@")
-	if len(infos) > 1 {
-		username = infos[0]
-	}
+	instanceTlsNew = smtp.NewServer(be)
 
-	_, err := db.Instance.Where("account =? and password =? and disabled=0", username, encodePwd).Get(&user)
-	if err != nil && err != sql.ErrNoRows {
-		log.Errorf("%+v", err)
+	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}}
 
-	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.Println("Starting Smtp With SSL Server Port:", instanceTlsNew.Addr)
+	if err := instanceTlsNew.ListenAndServeTLS(); err != nil {
+		log.Fatal(err)
 	}
-
-	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
-
 func StartWithTLS() {
 	be := &Backend{}
 
@@ -185,4 +103,8 @@ func Stop() {
 	if instanceTls != nil {
 		instanceTls.Close()
 	}
+
+	if instanceTlsNew != nil {
+		instanceTlsNew.Close()
+	}
 }

+ 2 - 2
server/main_test.go

@@ -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)
 	}
 }
 

+ 1 - 1
server/models/user_email.go

@@ -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')"`
 }
 

+ 1 - 0
server/res_init/init.go

@@ -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()

+ 3 - 1
server/services/list/list.go

@@ -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 {

+ 54 - 109
server/utils/send/send.go

@@ -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
-		}
-	}
-
-	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()
+	err, _ := doSend(ctx, config.Instance.Domains[0], b, to, e.From.EmailAddress)
 
-	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,33 +103,57 @@ 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 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)
-					}
+			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 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)
-						}
+			// 优先尝试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())
+
+			// 再试用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 {
 				log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
@@ -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 {

+ 4 - 0
server/utils/smtp/smtp.go

@@ -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,