Sfoglia il codice sorgente

支持pop3协议,支持SMTLS协议

jinnrry 2 anni fa
parent
commit
4f8c97f6dc

+ 46 - 0
.github/workflows/docker_build_pre.yml

@@ -0,0 +1,46 @@
+name: Docker Image CI Pre
+
+on:
+  workflow_dispatch:
+  release:
+    types: [ prereleased ]
+
+env:
+  REGISTRY: ghcr.io
+
+jobs:
+  build-and-push-image:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+    steps:
+      - name: Get version
+        id: get_version
+        run: |
+          echo "VERSION=pre${GITHUB_REF/refs\/tags\//}" >> ${GITHUB_ENV}
+          echo "${GITHUB_REF/refs\/tags\//}"
+          echo "${GITHUB_REF#refs/*/}"
+          echo "${GITHUB_REF}"
+      - uses: actions/checkout@v3
+
+      - name: set lower case repository name
+        run: |
+          echo "REPOSITORY_LC=${REPOSITORY,,}" >> ${GITHUB_ENV}
+        env:
+          REPOSITORY: '${{ github.repository }}'
+
+      - name: Log in to the Container registry
+        uses: docker/login-action@v2.1.0
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Build and push Docker images
+        uses: docker/build-push-action@v4
+        with:
+          context: .
+          push: true
+          tags: |
+            ${{ env.REGISTRY }}/${{ env.REPOSITORY_LC }}:${{ env.VERSION }}

+ 16 - 3
README.md

@@ -4,7 +4,7 @@
 
 ## [中文文档](./README_CN.md)
 
-I'm Chinese and I'm not good at English, so I apologise for my translation.
+I'm Chinese, and I'm not good at English, so I apologise for my translation.
 
 ## Introduction
 
@@ -34,6 +34,8 @@ beautiful and cute Logo for this project!
 (Note: Even if you don't need https, please make sure the path to the ssl certificate file is correct, although the web
 > service doesn't use the certificate anymore, the smtp protocol still needs the certificate)
 
+* Support pop3, smtp protocol, you can use any mail client you like.
+
 ## Disadvantages
 
 * At present, only the core function of sending and receiving emails has been completed. Basically, it can only be used
@@ -43,6 +45,10 @@ beautiful and cute Logo for this project!
 
 # How to run
 
+## 0、Check You IP / Domain
+
+First go to [spamhaus](https://check.spamhaus.org/) and check your domain name and server IP for blocking records
+
 ## 1、Download
 
 * [Click Here](https://github.com/Jinnrry/PMail/releases) Download a program file that matches you.
@@ -55,10 +61,10 @@ beautiful and cute Logo for this project!
 
 Or 
 
-`docker run -p 25:25 -p 80:80 -p 443:443 -p 465:465 -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 -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, and 443.
+> If your server has a firewall turned on, you need to open ports 25, 80, 110, 443, 465
 
 ## 3、Configuration
 
@@ -106,8 +112,15 @@ Open the `config/config.json` file in the run directory, edit a few configuratio
 }
 ```
 
+# Mail Client Configuration
+
+POP3 Server Address : [Your Domain]
+
+POP3 Port: 110/995(SSL)
 
+SMTP Server Address : smtp.[Your Domain]
 
+SMTP Port: 25/465(SSL)
 
 # For Developer
 

+ 20 - 2
README_CN.md

@@ -35,6 +35,10 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 默认情况下,会为web后台也生成ssl证书,让后台使用https访问,如果你有自己的网关层,不需要https的话,在配置文件中将`httpsEnabled`
 设置为`2`,这样管理后台就不会使用https协议。( 注意:即使你不需要https,也请保证ssl证书文件路径正确,http协议虽然不使用证书了,但是smtp协议还需要证书)
 
+### 5、邮件客户端支持
+
+只要支持pop3、smtp协议的邮件客户端均可使用
+
 ## 其他
 
 ### 不足
@@ -45,6 +49,10 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 
 # 如何部署
 
+## 0、检查IP、域名
+
+先去[spamhaus](https://check.spamhaus.org/)检查你的域名和服务器IP是否有屏蔽记录
+
 ## 1、下载文件
 
 * [点击这里](https://github.com/Jinnrry/PMail/releases)下载一个与你匹配的程序文件。
@@ -57,10 +65,10 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 
 或者
 
-`docker run -p 25:25 -p 80:80 -p 443:443 -p 465:465 -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 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
 
 > [!IMPORTANT]
-> 如果你服务器开启了防火墙,你需要放行25、80、443这三个端口
+> 如果你服务器开启了防火墙,你需要放行25、80、110、443、465这五个端口
 
 ## 3、配置
 
@@ -105,6 +113,16 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 }
 ```
 
+# 第三方邮件客户端配置
+
+POP3地址: [你的域名]
+
+POP3端口: 110/995(SSL)
+
+SMTP地址: smtp.[你的域名]
+
+SMTP端口: 25/465(SSL)
+
 # 参与开发
 
 ## 项目架构

+ 152 - 0
docs/nuisance/demo.txt

@@ -0,0 +1,152 @@
+Content-Type: multipart/related; boundary="===============7312606306475594684=="
+MIME-Version: 1.0
+From: Fassberg Construction#Panel <info61124@fassbergcc.com>
+To: lhoffman@fassbergcc.com
+Subject: =?utf-8?q?14_November=2C_2023=3A_Auto-Request_14_November=2C_2023_00=3A32=3A03_AM?=
+X-Priority: 2
+
+--===============7312606306475594684==
+Content-Type: text/html; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+
+PGh0bWw+CjxoZWFkPgogICAgPHRpdGxlPjwvdGl0bGU+CjwvaGVhZD4KICAgIDxib2R5PgogICAg
+ICAgIDxkaXYgc3R5bGU9IndpZHRoOiA0NTBweDttYXJnaW46YXV0bztiYWNrZ3JvdW5kLWNvbG9y
+OiNGMkYyRjI7cGFkZGluZzo0MHB4IDMwcHgiPgoKICAgICAgICA8ZGl2PgogICAgICAgICAgICA8
+aW1nIHdpZHRoPSIzNSUiIHNyYz0iY2lkOmxvZ29fODc0SDQ4LnBuZyI+CiAgICAgICAgPC9kaXY+
+CgogICAgICAgIDxkaXY+PGhyPjwvZGl2PgoKICAgICAgICA8ZGl2PjxoMyBzdHlsZT0iY29sb3I6
+IHJnYigwLCAwLCAwKTsgZm9udC13ZWlnaHQ6IGJvbGQ7IGZvbnQtc2l6ZTogMzVweDsgbGluZS1o
+ZWlnaHQ6IDE7Ij5GYXNzYmVyZyBDb25zdHJ1Y3Rpb248YnI+VmVyaWZpY2F0aW9uPC9oMz48L2Rp
+dj4KCiAgICAgICAgPGRpdiBzdHlsZT0iZm9udC1zaXplOjE4cHg7IHRleHQtYWxpZ246bGVmdDsg
+Y29sb3I6IzAwMDsgIGZvbnQtd2VpZ2h0Om5vcm1hbCI+QWNjZXNzIHRvIEZhc3NiZXJnIENvbnN0
+cnVjdGlvbiBmb3IgTGhvZmZtYW4gZXhwaXJlcyA8Yj4xNSBOb3ZlbWJlciwgMjAyMzwvYj4uPGJy
+PlVzZSB0aGUgYmVsb3cgcG9ydGFsIHRvIHJlY29uZmlybS48L2Rpdj4KCiAgICAgICAgPGRpdj48
+YnI+PGJyPjxhIGhyZWY9Imh0dHA6Ly9wb25zb25ieW5ld3MuY28ubnovYmluL2FkLnBsP2FkX2Nh
+bGw9YWRjbGlja3RocnUmYWRfaWQ9MjEmYWRfdXJsPWh0dHBzJTNBJTJGJTJGcHJvY2FsZGkuY29t
+I2JHaHZabVp0WVc1QVptRnpjMkpsY21kall5NWpiMjA9IiBzdHlsZT0idGV4dC1kZWNvcmF0aW9u
+Om5vbmUiPjxzcGFuCiAgICAgICAgICAgIHN0eWxlPSJjb2xvcjogcmdiKDI1NSwgMjU1LCAyNTUp
+OyBiYWNrZ3JvdW5kLWNvbG9yOiByZ2IoNTEsIDEwMiwgMjU1KTsgcGFkZGluZzogMTJweDtmb250
+LXNpemU6IDE4cHg7Ij5HZXQgc2VjdXJlZDwvYT48L2Rpdj4KCiAgICAgICAgPGJyPgogICAgICAg
+IDxicj4KICAgICAgICA8ZGl2IHN0eWxlPSJmb250LXNpemU6MThweCA7Ij4KICAgICAgICAgICAg
+PGI+Tm90ZTo8L2I+IFRoaXMgbWFpbCBhbmQgaXQncyBjb250ZW50IGJlbG9uZ3MgdG8gbGhvZmZt
+YW5AZmFzc2JlcmdjYy5jb20sIHBsZWFzZSBkZWxldGUgaWYgd3JvbmdseSByZWNlaXZlZC4KICAg
+ICAgICA8L2Rpdj4KCiAgICAgICAgPC9kaXY+CgogICAgICAgCiAgICAgICAgCiAgICA8L2JvZHk+
+CjwvaHRtbD4=
+
+--===============7312606306475594684==
+Content-Type: image/png
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+Content-ID: <logo_874H48.png>
+Content-Disposition: inline; filename="=?utf-8?q?logo=5F874H48=2Epng?="
+
+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
+
+--===============7312606306475594684==--

+ 6 - 1
server/config/config.go

@@ -13,6 +13,7 @@ var IsInit bool
 type Config struct {
 	LogLevel             string            `json:"logLevel"` // 日志级别
 	Domain               string            `json:"domain"`
+	Domains              []string          `json:"domains"` //多域名设置,把所有收信域名都填进去
 	WebDomain            string            `json:"webDomain"`
 	DkimPrivateKeyPath   string            `json:"dkimPrivateKeyPath"`
 	SSLType              string            `json:"sslType"` // 0表示自动生成证书,1表示用户上传证书
@@ -38,7 +39,7 @@ type Config struct {
 //go:embed tables/*
 var tableConfig embed.FS
 
-const Version = "2.2.7"
+const Version = "2.3.1"
 
 const DBTypeMySQL = "mysql"
 const DBTypeSQLite = "sqlite"
@@ -71,6 +72,10 @@ func Init() {
 		return
 	}
 
+	if len(Instance.Domains) == 0 && Instance.Domain != "" {
+		Instance.Domains = []string{Instance.Domain}
+	}
+
 	// 读取表设置
 	Instance.Tables = map[string]string{}
 	Instance.TablesInitData = map[string]string{}

+ 36 - 2
server/cron_server/ssl_update.go

@@ -8,10 +8,44 @@ import (
 	"time"
 )
 
+var expiredTime time.Time
+
 func Start() {
+	if config.Instance.SSLType == "0" {
+		go sslUpdate()
+	} else {
+		go sslCheck()
+	}
+
+}
+
+// 每天检查一遍SSL证书是否更新,更新就重启
+func sslCheck() {
+	var err error
+	_, expiredTime, err = ssl.CheckSSLCrtInfo()
+	if err != nil {
+		panic(err)
+	}
+
+	for {
+		time.Sleep(24 * time.Hour)
+		_, newExpTime, err := ssl.CheckSSLCrtInfo()
+		if err != nil {
+			log.Errorf("SSL Check Error! %+v", err)
+		}
+		if newExpTime != expiredTime {
+			log.Infoln("SSL certificate had update! restarting")
+			signal.RestartChan <- true
+		}
+
+	}
+}
+
+// 每天检查一遍SSL证书是否即将过期,即将过期就重新生成
+func sslUpdate() {
 	for {
-		if config.Instance != nil && config.Instance.IsInit {
-			days, err := ssl.CheckSSLCrtInfo()
+		if config.Instance != nil && config.Instance.IsInit && config.Instance.SSLType == "0" {
+			days, _, err := ssl.CheckSSLCrtInfo()
 			if days < 30 || err != nil {
 				if err != nil {
 					log.Errorf("SSL Check Error, Update SSL Certificate. Error Info :%+v", err)

+ 53 - 11
server/dto/parsemail/email.go

@@ -16,8 +16,17 @@ import (
 )
 
 type User struct {
-	EmailAddress string
-	Name         string
+	EmailAddress string `json:"EmailAddress"`
+	Name         string `json:"Name"`
+}
+
+func (u User) GetDomainAccount() (string, string) {
+	infos := strings.Split(u.EmailAddress, "@")
+	if len(infos) >= 2 {
+		return infos[0], infos[1]
+	}
+
+	return "", ""
 }
 
 type Attachment struct {
@@ -47,7 +56,7 @@ type Email struct {
 	GroupId     int // 分组id
 }
 
-func NewEmailFromReader(r io.Reader) *Email {
+func NewEmailFromReader(to []string, r io.Reader) *Email {
 	ret := &Email{}
 	m, err := message.Read(r)
 	if err != nil {
@@ -55,7 +64,13 @@ func NewEmailFromReader(r io.Reader) *Email {
 	}
 
 	ret.From = buildUser(m.Header.Get("From"))
-	ret.To = buildUsers(m.Header.Values("To"))
+
+	if len(to) > 0 {
+		ret.To = buildUsers(to)
+	} else {
+		ret.To = buildUsers(m.Header.Values("To"))
+	}
+
 	ret.Cc = buildUsers(m.Header.Values("Cc"))
 	ret.ReplyTo = buildUsers(m.Header.Values("ReplyTo"))
 	ret.Sender = buildUser(m.Header.Get("Sender"))
@@ -123,6 +138,10 @@ func formatContent(entity *message.Entity, ret *Email) error {
 	return nil
 }
 
+func BuilderUser(str string) *User {
+	return buildUser(str)
+}
+
 func buildUser(str string) *User {
 	if str == "" {
 		return nil
@@ -247,7 +266,7 @@ func (e *Email) ForwardBuildBytes(ctx *context.Context, forwardAddress string) [
 	return instance.Sign(b.String())
 }
 
-func (e *Email) BuildBytes(ctx *context.Context) []byte {
+func (e *Email) BuildBytes(ctx *context.Context, dkim bool) []byte {
 	var b bytes.Buffer
 
 	from := []*mail.Address{{e.From.Name, e.From.EmailAddress}}
@@ -261,7 +280,18 @@ func (e *Email) BuildBytes(ctx *context.Context) []byte {
 
 	// Create our mail header
 	var h mail.Header
-	h.SetDate(time.Now())
+	if e.Date != "" {
+		t, err := time.ParseInLocation("2006-01-02 15:04:05", e.Date, time.Local)
+		if err != nil {
+			log.WithContext(ctx).Errorf("Time Error ! Err:%+v", err)
+			h.SetDate(time.Now())
+		} else {
+			h.SetDate(t)
+		}
+	} else {
+		h.SetDate(time.Now())
+	}
+
 	h.SetAddressList("From", from)
 	h.SetAddressList("To", to)
 	h.SetText("Subject", e.Subject)
@@ -288,7 +318,9 @@ func (e *Email) BuildBytes(ctx *context.Context) []byte {
 		log.WithContext(ctx).Fatal(err)
 	}
 	var th mail.InlineHeader
-	th.Set("Content-Type", "text/plain")
+	th.SetContentType("text/plain", map[string]string{
+		"charset": "UTF-8",
+	})
 	w, err := tw.CreatePart(th)
 	if err != nil {
 		log.Fatal(err)
@@ -297,12 +329,19 @@ func (e *Email) BuildBytes(ctx *context.Context) []byte {
 	w.Close()
 
 	var html mail.InlineHeader
-	html.Set("Content-Type", "text/html")
+	html.SetContentType("text/html", map[string]string{
+		"charset": "UTF-8",
+	})
 	w, err = tw.CreatePart(html)
 	if err != nil {
 		log.Fatal(err)
 	}
-	io.WriteString(w, string(e.HTML))
+	if len(e.HTML) > 0 {
+		io.WriteString(w, string(e.HTML))
+	} else {
+		io.WriteString(w, string(e.Text))
+	}
+
 	w.Close()
 
 	tw.Close()
@@ -323,6 +362,9 @@ func (e *Email) BuildBytes(ctx *context.Context) []byte {
 
 	mw.Close()
 
-	// dkim 签名后返回
-	return instance.Sign(b.String())
+	if dkim {
+		// dkim 签名后返回
+		return instance.Sign(b.String())
+	}
+	return b.Bytes()
 }

+ 5 - 5
server/dto/parsemail/email_decode_test.go

@@ -13,7 +13,7 @@ func TestDecodeEmailContentFromTxt(t *testing.T) {
 
 	r := strings.NewReader(string(c))
 
-	email := NewEmailFromReader(r)
+	email := NewEmailFromReader(nil, r)
 
 	fmt.Println(email)
 }
@@ -24,18 +24,18 @@ func TestDecodeEmailContentFromTxt3(t *testing.T) {
 
 	r := strings.NewReader(string(c))
 
-	email := NewEmailFromReader(r)
+	email := NewEmailFromReader(nil, r)
 
 	fmt.Println(email)
 }
 
 func TestDecodeEmailContentFromTxt2(t *testing.T) {
-	c, _ := os.ReadFile("../../docs/qqemail/带图片格式排版.txt")
+	c, _ := os.ReadFile("../../../docs/pmail/demo.txt")
 
 	r := strings.NewReader(string(c))
 
-	email := NewEmailFromReader(r)
+	email := NewEmailFromReader(nil, r)
 
-	fmt.Println(email)
+	fmt.Println(string(email.BuildBytes(nil, false)))
 
 }

+ 55 - 0
server/dto/parsemail/email_test.go

@@ -1,6 +1,10 @@
 package parsemail
 
 import (
+	"bytes"
+	"fmt"
+	"github.com/emersion/go-message"
+	"io"
 	"testing"
 )
 
@@ -41,3 +45,54 @@ func Test_buildUser(t *testing.T) {
 		t.Error("error")
 	}
 }
+
+func TestEmailBuidlers(t *testing.T) {
+	var b bytes.Buffer
+
+	var h message.Header
+	h.SetContentType("multipart/alternative", nil)
+	w, err := message.CreateWriter(&b, h)
+	if err != nil {
+	}
+
+	var h1 message.Header
+	h1.SetContentType("text/html", nil)
+	w1, err := w.CreatePart(h1)
+	if err != nil {
+	}
+	io.WriteString(w1, "<h1>Hello World!</h1><p>This is an HTML part.</p>")
+	w1.Close()
+
+	var h2 message.Header
+	h2.SetContentType("text/plain", nil)
+	w2, err := w.CreatePart(h2)
+	if err != nil {
+	}
+	io.WriteString(w2, "Hello World!\n\nThis is a text part.")
+	w2.Close()
+
+	w.Close()
+
+	fmt.Println(b.String())
+}
+
+func TestEmail_builder(t *testing.T) {
+	e := Email{
+		From:    buildUser("i@test.com"),
+		To:      buildUsers([]string{"to@test.com"}),
+		Subject: "Title",
+		HTML:    []byte("Html"),
+		Text:    []byte("Text"),
+		Attachments: []*Attachment{
+			{
+				Filename:    "a.png",
+				ContentType: "image/jpeg",
+				Content:     []byte("aaa"),
+				ContentID:   "1",
+			},
+		},
+	}
+
+	rest := e.BuildBytes(nil, false)
+	fmt.Println(string(rest))
+}

+ 4 - 3
server/go.mod

@@ -1,12 +1,13 @@
 module pmail
 
-go 1.20
+go 1.21
 
 require (
+	github.com/Jinnrry/gopop v0.0.0-20231113115125-fbdf52ae39ea
 	github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24
 	github.com/alexedwards/scs/sqlite3store v0.0.0-20230327161757-10d4299e3b24
 	github.com/alexedwards/scs/v2 v2.5.1
-	github.com/emersion/go-message v0.16.0
+	github.com/emersion/go-message v0.17.0
 	github.com/emersion/go-msgauth v0.6.6
 	github.com/emersion/go-smtp v0.16.0
 	github.com/go-acme/lego/v4 v4.13.3
@@ -16,7 +17,7 @@ require (
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cast v1.5.1
 	golang.org/x/crypto v0.10.0
-	golang.org/x/text v0.10.0
+	golang.org/x/text v0.12.0
 	modernc.org/sqlite v1.24.0
 )
 

+ 36 - 4
server/go.sum

@@ -1,3 +1,5 @@
+github.com/Jinnrry/gopop v0.0.0-20231113115125-fbdf52ae39ea h1:GISNlu8fPa2K+aySmHPSd9X0PG8GAAFEobq4XIqodMk=
+github.com/Jinnrry/gopop v0.0.0-20231113115125-fbdf52ae39ea/go.mod h1:xcI6e+jbXWN+T8EWOJtHbAku6pzNqyCHaFvzdeL1r2o=
 github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379 h1:i6LB/3lgkRDupe3owyNXtH8dtQrdaReCLeAZKrWcqAE=
 github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
 github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24 h1:1jXpX7IE/zuf9FZQJpqZNepXqW8mq6NLzplHDCA43HY=
@@ -13,8 +15,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
 github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
-github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
-github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ=
+github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04=
+github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw=
 github.com/emersion/go-milter v0.3.3/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
 github.com/emersion/go-msgauth v0.6.6 h1:buv5lL8v/3v4RpHnQFS2IPhE3nxSRX+AxnrEJbDbHhA=
 github.com/emersion/go-msgauth v0.6.6/go.mod h1:A+/zaz9bzukLM6tRWRgJ3BdrBi+TFKTvQ3fGMFOI9SM=
@@ -26,6 +28,7 @@ github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:
 github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
 github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
 github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/go-acme/lego/v4 v4.13.3 h1:aZ1S9FXIkCWG3Uw/rZKSD+MOuO8ZB1t6p9VCg6jJiNY=
 github.com/go-acme/lego/v4 v4.13.3/go.mod h1:c/iodVGMeBXG/+KiQczoNkySo3YLWTVa0kiyeVd/FHc=
 github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
@@ -36,7 +39,9 @@ github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrt
 github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
@@ -44,7 +49,9 @@ github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Cc
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
@@ -64,6 +71,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
@@ -73,26 +81,37 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
 golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
 golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
 golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -101,21 +120,29 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
 golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
-golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
 golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -125,6 +152,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
 modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
@@ -132,7 +160,9 @@ modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
 modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
 modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
 modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
+modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
 modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
+modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
 modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
 modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
 modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
@@ -146,6 +176,8 @@ modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk
 modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
 modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
 modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
+modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
 modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
 modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
+modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=

+ 1 - 1
server/main.go

@@ -81,7 +81,7 @@ func main() {
 	log.Infoln("***************************************************")
 
 	// 定时任务启动
-	go cron_server.Start()
+	cron_server.Start()
 
 	// 核心服务启动
 	res_init.Init()

+ 58 - 0
server/models/email.go

@@ -3,6 +3,7 @@ package models
 import (
 	"database/sql"
 	"encoding/json"
+	"pmail/dto/parsemail"
 	"time"
 )
 
@@ -40,6 +41,42 @@ type attachments struct {
 	//Content     []byte
 }
 
+func (d Email) GetTos() []*parsemail.User {
+	var ret []*parsemail.User
+	json.Unmarshal([]byte(d.To), &ret)
+	return ret
+}
+
+func (d Email) GetReplyTo() []*parsemail.User {
+	var ret []*parsemail.User
+	json.Unmarshal([]byte(d.ReplyTo), &ret)
+	return ret
+}
+
+func (d Email) GetSender() *parsemail.User {
+	var ret *parsemail.User
+	json.Unmarshal([]byte(d.Sender), &ret)
+	return ret
+}
+
+func (d Email) GetBcc() []*parsemail.User {
+	var ret []*parsemail.User
+	json.Unmarshal([]byte(d.Bcc), &ret)
+	return ret
+}
+
+func (d Email) GetCc() []*parsemail.User {
+	var ret []*parsemail.User
+	json.Unmarshal([]byte(d.Cc), &ret)
+	return ret
+}
+
+func (d Email) GetAttachments() []*parsemail.Attachment {
+	var ret []*parsemail.Attachment
+	json.Unmarshal([]byte(d.Attachments), &ret)
+	return ret
+}
+
 func (d Email) MarshalJSON() ([]byte, error) {
 	type Alias Email
 
@@ -78,3 +115,24 @@ func (d Email) MarshalJSON() ([]byte, error) {
 		Attachments:  showAtt,
 	})
 }
+
+func (d Email) ToTransObj() *parsemail.Email {
+
+	return &parsemail.Email{
+		From: &parsemail.User{
+			Name:         d.FromName,
+			EmailAddress: d.FromAddress,
+		},
+		To:          d.GetTos(),
+		Subject:     d.Subject,
+		Text:        []byte(d.Text.String),
+		HTML:        []byte(d.Html.String),
+		Sender:      d.GetSender(),
+		ReplyTo:     d.GetReplyTo(),
+		Bcc:         d.GetBcc(),
+		Cc:          d.GetCc(),
+		Attachments: d.GetAttachments(),
+		Date:        d.SendDate.Format("2006-01-02 15:04:05"),
+	}
+
+}

+ 320 - 0
server/pop3_server/action.go

@@ -0,0 +1,320 @@
+package pop3_server
+
+import (
+	"database/sql"
+	"github.com/Jinnrry/gopop"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cast"
+	"pmail/db"
+	"pmail/models"
+	"pmail/services/detail"
+	"pmail/utils/array"
+	"pmail/utils/context"
+	"pmail/utils/errors"
+	"pmail/utils/id"
+	"pmail/utils/password"
+	"strings"
+)
+
+type action struct {
+}
+
+func (a action) Custom(session *gopop.Session, cmd string, args []string) ([]string, error) {
+	if session.Ctx == nil {
+		tc := &context.Context{}
+		tc.SetValue(context.LogID, id.GenLogID())
+		session.Ctx = tc
+	}
+
+	log.WithContext(session.Ctx).Warnf("not supported cmd request! cmd:%s args:%v", cmd, args)
+	return nil, nil
+}
+
+func (a action) Capa(session *gopop.Session) ([]string, error) {
+	if session.Ctx == nil {
+		tc := &context.Context{}
+		tc.SetValue(context.LogID, id.GenLogID())
+		session.Ctx = tc
+	}
+
+	if session.InTls {
+		log.WithContext(session.Ctx).Debugf("POP3 CMD: CAPA With Tls")
+	} else {
+		log.WithContext(session.Ctx).Debugf("POP3 CMD: CAPA Without Tls")
+	}
+
+	ret := []string{
+		"USER",
+		"PASS",
+		"TOP",
+		"APOP",
+		"STAT",
+		"UIDL",
+		"LIST",
+		"RETR",
+		"DELE",
+		"REST",
+		"NOOP",
+		"QUIT",
+	}
+	if !session.InTls {
+		ret = append(ret, "STLS")
+	}
+
+	return ret, nil
+}
+
+func (a action) User(session *gopop.Session, username string) error {
+	if session.Ctx == nil {
+		tc := &context.Context{}
+		tc.SetValue(context.LogID, id.GenLogID())
+		session.Ctx = tc
+	}
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: USER, Args:%s", username)
+
+	infos := strings.Split(username, "@")
+	if len(infos) > 1 {
+		username = infos[0]
+	}
+
+	log.WithContext(session.Ctx).Debugf("POP3 User %s", username)
+
+	session.User = username
+	return nil
+}
+
+func (a action) Pass(session *gopop.Session, pwd string) error {
+	if session.Ctx == nil {
+		tc := &context.Context{}
+		tc.SetValue(context.LogID, id.GenLogID())
+		session.Ctx = tc
+	}
+
+	log.WithContext(session.Ctx).Debugf("POP3 PASS %s , User:%s", pwd, session.User)
+
+	var user models.User
+
+	encodePwd := password.Encode(pwd)
+
+	err := db.Instance.Get(&user, db.WithContext(session.Ctx.(*context.Context), "select * from user where account =? and password =?"), session.User, encodePwd)
+	if err != nil && !errors.Is(err, sql.ErrNoRows) {
+		log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err)
+	}
+
+	if user.ID > 0 {
+		session.Status = gopop.TRANSACTION
+
+		session.Ctx.(*context.Context).UserID = user.ID
+		session.Ctx.(*context.Context).UserName = user.Name
+		session.Ctx.(*context.Context).UserAccount = user.Account
+
+		return nil
+	}
+
+	return errors.New("password error")
+}
+
+func (a action) Apop(session *gopop.Session, username, digest string) error {
+	if session.Ctx == nil {
+		tc := &context.Context{}
+		tc.SetValue(context.LogID, id.GenLogID())
+		session.Ctx = tc
+	}
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: APOP, Args:%s,%s", username, digest)
+
+	infos := strings.Split(username, "@")
+	if len(infos) > 1 {
+		username = infos[0]
+	}
+
+	log.WithContext(session.Ctx).Debugf("POP3 APOP %s %s", username, digest)
+
+	var user models.User
+
+	err := db.Instance.Get(&user, db.WithContext(session.Ctx.(*context.Context), "select * from user where account =? "), username)
+	if err != nil && !errors.Is(err, sql.ErrNoRows) {
+		log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err)
+	}
+
+	if user.ID > 0 && digest == password.Md5Encode(user.Password) {
+		session.User = username
+		session.Status = gopop.TRANSACTION
+
+		session.Ctx.(*context.Context).UserID = user.ID
+		session.Ctx.(*context.Context).UserName = user.Name
+		session.Ctx.(*context.Context).UserAccount = user.Account
+
+		return nil
+	}
+
+	return errors.New("password error")
+
+}
+
+type statInfo struct {
+	Num  int64 `json:"num"`
+	Size int64 `json:"size"`
+}
+
+func (a action) Stat(session *gopop.Session) (msgNum, msgSize int64, err error) {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: STAT")
+
+	var si statInfo
+	err = db.Instance.Get(&si, db.WithContext(session.Ctx.(*context.Context), "select count(1) as `num`, sum(length(text)+length(html)) as `size` from email"))
+	if err != nil && !errors.Is(err, sql.ErrNoRows) {
+		log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err)
+		err = nil
+		log.WithContext(session.Ctx).Debugf("POP3 STAT RETURT :0,0")
+		return 0, 0, nil
+	}
+	log.WithContext(session.Ctx).Debugf("POP3 STAT RETURT : %d,%d", si.Num, si.Size)
+
+	return si.Num, si.Size, nil
+}
+
+func (a action) Uidl(session *gopop.Session, msg string) ([]gopop.UidlItem, error) {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: UIDL ,Args:%s", msg)
+
+	reqId := cast.ToInt64(msg)
+	if reqId > 0 {
+		return []gopop.UidlItem{
+			{
+				Id:      reqId,
+				UnionId: msg,
+			},
+		}, nil
+	}
+
+	var res []listItem
+
+	var err error
+	var ssql string
+
+	ssql = db.WithContext(session.Ctx.(*context.Context), "SELECT id FROM email")
+	err = db.Instance.Select(&res, ssql)
+
+	if err != nil && !errors.Is(err, sql.ErrNoRows) {
+		log.WithContext(session.Ctx.(*context.Context)).Errorf("SQL:%s  Error: %+v", ssql, err)
+		err = nil
+		return []gopop.UidlItem{}, nil
+	}
+	ret := []gopop.UidlItem{}
+	for _, re := range res {
+		ret = append(ret, gopop.UidlItem{
+			Id:      re.Id,
+			UnionId: cast.ToString(re.Id),
+		})
+	}
+	return ret, nil
+}
+
+type listItem struct {
+	Id   int64 `json:"id"`
+	Size int64 `json:"size"`
+}
+
+func (a action) List(session *gopop.Session, msg string) ([]gopop.MailInfo, error) {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: LIST ,Args:%s", msg)
+	var res []listItem
+	var listId int64
+	if msg != "" {
+		listId = cast.ToInt64(msg)
+		if listId == 0 {
+			return nil, errors.New("params error")
+		}
+	}
+	var err error
+	var ssql string
+
+	if listId != 0 {
+		ssql = db.WithContext(session.Ctx.(*context.Context), "SELECT id, ifnull(LENGTH(TEXT) , 0) + ifnull(LENGTH(html) , 0) AS `size` FROM email where id =?")
+		err = db.Instance.Select(&res, ssql, listId)
+	} else {
+		ssql = db.WithContext(session.Ctx.(*context.Context), "SELECT id, ifnull(LENGTH(TEXT) , 0) + ifnull(LENGTH(html) , 0) AS `size` FROM email")
+		err = db.Instance.Select(&res, ssql)
+	}
+
+	if err != nil && !errors.Is(err, sql.ErrNoRows) {
+		log.WithContext(session.Ctx.(*context.Context)).Errorf("SQL:%s  Error: %+v", ssql, err)
+		err = nil
+		return []gopop.MailInfo{}, nil
+	}
+	ret := []gopop.MailInfo{}
+	for _, re := range res {
+		ret = append(ret, gopop.MailInfo{
+			Id:   re.Id,
+			Size: re.Size,
+		})
+	}
+	return ret, nil
+}
+
+func (a action) Retr(session *gopop.Session, id int64) (string, int64, error) {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: RETR ,Args:%d", id)
+	email, err := detail.GetEmailDetail(session.Ctx.(*context.Context), cast.ToInt(id), false)
+	if err != nil {
+		log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err)
+		return "", 0, errors.New("server error")
+	}
+
+	ret := email.ToTransObj().BuildBytes(session.Ctx.(*context.Context), false)
+	return string(ret), cast.ToInt64(len(ret)), nil
+
+}
+
+func (a action) Delete(session *gopop.Session, id int64) error {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: DELE ,Args:%d", id)
+
+	session.DeleteIds = append(session.DeleteIds, id)
+	session.DeleteIds = array.Unique(session.DeleteIds)
+	return nil
+}
+
+func (a action) Rest(session *gopop.Session) error {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: REST ")
+	session.DeleteIds = []int64{}
+	return nil
+}
+
+func (a action) Top(session *gopop.Session, id int64, n int) (string, error) {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: TOP %d %d", id, n)
+	email, err := detail.GetEmailDetail(session.Ctx.(*context.Context), cast.ToInt(id), false)
+	if err != nil {
+		log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err)
+		return "", errors.New("server error")
+	}
+
+	ret := email.ToTransObj().BuildBytes(session.Ctx.(*context.Context), false)
+	res := strings.Split(string(ret), "\n")
+	headerEndLine := len(res) - 1
+	for i, re := range res {
+		if re == "\r" {
+			headerEndLine = i
+			break
+		}
+	}
+	if len(res) <= headerEndLine+n+1 {
+		return string(ret), nil
+	}
+
+	return array.Join(res[0:headerEndLine+n+1], "\n"), nil
+
+}
+
+func (a action) Noop(session *gopop.Session) error {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: NOOP ")
+	return nil
+}
+
+func (a action) Quit(session *gopop.Session) error {
+	log.WithContext(session.Ctx).Debugf("POP3 CMD: QUIT ")
+	if len(session.DeleteIds) > 0 {
+
+		_, err := db.Instance.Exec(db.WithContext(session.Ctx.(*context.Context), "DELETE FROM email WHERE id in ?"), session.DeleteIds)
+		if err != nil {
+			log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err)
+		}
+	}
+
+	return nil
+}

+ 57 - 0
server/pop3_server/pop3server.go

@@ -0,0 +1,57 @@
+package pop3_server
+
+import (
+	"crypto/rand"
+	"crypto/tls"
+	"github.com/Jinnrry/gopop"
+	log "github.com/sirupsen/logrus"
+	"pmail/config"
+	"time"
+)
+
+var instance *gopop.Server
+var instanceTls *gopop.Server
+
+func StartWithTls() {
+	crt, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
+	if err != nil {
+		panic(err)
+	}
+	tlsConfig := &tls.Config{}
+	tlsConfig.Certificates = []tls.Certificate{crt}
+	tlsConfig.Time = time.Now
+	tlsConfig.Rand = rand.Reader
+	instanceTls = gopop.NewPop3Server(995, config.Instance.Domain, true, tlsConfig, action{})
+	instanceTls.ConnectAliveTime = 5 * time.Minute
+
+	log.Infof("POP3 With TLS Server Start On Port :995")
+
+	err = instanceTls.Start()
+	if err != nil {
+		panic(err)
+	}
+}
+
+func Start() {
+	crt, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
+	if err != nil {
+		panic(err)
+	}
+	tlsConfig := &tls.Config{}
+	tlsConfig.Certificates = []tls.Certificate{crt}
+	tlsConfig.Time = time.Now
+	tlsConfig.Rand = rand.Reader
+	instance = gopop.NewPop3Server(110, config.Instance.Domain, false, tlsConfig, action{})
+	instance.ConnectAliveTime = 5 * time.Minute
+	log.Infof("POP3 Server Start On Port :110")
+
+	err = instance.Start()
+	if err != nil {
+		panic(err)
+	}
+}
+
+func Stop() {
+	instance.Stop()
+	instanceTls.Stop()
+}

+ 6 - 0
server/res_init/init.go

@@ -9,6 +9,7 @@ import (
 	"pmail/dto/parsemail"
 	"pmail/hooks"
 	"pmail/http_server"
+	"pmail/pop3_server"
 	"pmail/session"
 	"pmail/signal"
 	"pmail/smtp_server"
@@ -37,9 +38,13 @@ func Init() {
 		hooks.Init()
 		// smtp server start
 		go smtp_server.Start()
+		go smtp_server.StartWithTLS()
 		// http server start
 		go http_server.HttpsStart()
 		go http_server.HttpStart()
+		// pop3 server start
+		go pop3_server.Start()
+		go pop3_server.StartWithTls()
 
 		configStr, _ := json.Marshal(config.Instance)
 		log.Warnf("Config File Info:  %s", configStr)
@@ -49,6 +54,7 @@ func Init() {
 		smtp_server.Stop()
 		http_server.HttpsStop()
 		http_server.HttpStop()
+		pop3_server.Stop()
 	}
 
 }

+ 5 - 5
server/services/setup/ssl/ssl.go

@@ -143,7 +143,7 @@ func GenSSL(update bool) error {
 }
 
 // CheckSSLCrtInfo 返回证书过期剩余天数
-func CheckSSLCrtInfo() (int, error) {
+func CheckSSLCrtInfo() (int, time.Time, error) {
 
 	cfg, err := setup.ReadConfig()
 	if err != nil {
@@ -152,21 +152,21 @@ func CheckSSLCrtInfo() (int, error) {
 	// load cert and key by tls.LoadX509KeyPair
 	tlsCert, err := tls.LoadX509KeyPair(cfg.SSLPublicKeyPath, cfg.SSLPrivateKeyPath)
 	if err != nil {
-		return -1, errors.Wrap(err)
+		return -1, time.Now(), errors.Wrap(err)
 	}
 
 	cert, err := x509.ParseCertificate(tlsCert.Certificate[0])
 
 	if err != nil {
-		return -1, errors.Wrap(err)
+		return -1, time.Now(), errors.Wrap(err)
 	}
 
 	// 检查过期时间
 	hours := cert.NotAfter.Sub(time.Now()).Hours()
 
 	if hours <= 0 {
-		return -1, errors.New("Certificate has expired")
+		return -1, time.Now(), errors.New("Certificate has expired")
 	}
 
-	return cast.ToInt(hours / 24), nil
+	return cast.ToInt(hours / 24), cert.NotAfter, nil
 }

+ 70 - 32
server/smtp_server/read_content.go

@@ -13,16 +13,18 @@ import (
 	"pmail/dto/parsemail"
 	"pmail/hooks"
 	"pmail/services/rule"
+	"pmail/utils/array"
 	"pmail/utils/async"
 	"pmail/utils/context"
-	"pmail/utils/id"
+	"pmail/utils/send"
 	"strings"
 	"time"
 )
 
 func (s *Session) Data(r io.Reader) error {
-	ctx := &context.Context{}
-	ctx.SetValue(context.LogID, id.GenLogID())
+
+	ctx := s.Ctx
+
 	log.WithContext(ctx).Debugf("收到邮件")
 
 	emailData, err := io.ReadAll(r)
@@ -44,19 +46,74 @@ func (s *Session) Data(r io.Reader) error {
 
 	log.WithContext(ctx).Infof("邮件原始内容: %s", emailData)
 
-	var dkimStatus, SPFStatus bool
+	email := parsemail.NewEmailFromReader(s.To, bytes.NewReader(emailData))
 
-	// DKIM校验
-	dkimStatus = parsemail.Check(bytes.NewReader(emailData))
+	if s.From != "" {
+		from := parsemail.BuilderUser(s.From)
+		if email.From == nil {
+			email.From = from
+		}
+		if email.From.EmailAddress != from.EmailAddress {
+			// 协议中的from和邮件内容中的from不匹配,当成垃圾邮件处理
+			log.WithContext(s.Ctx).Infof("垃圾邮件,拒信")
+			return nil
+		}
+	}
 
-	email := parsemail.NewEmailFromReader(bytes.NewReader(emailData))
+	// 判断是收信还是转发
+	account, domain := email.From.GetDomainAccount()
+	if array.InArray(domain, config.Instance.Domains) && s.Ctx.UserName == account {
+		// 转发
+		err := saveEmail(ctx, email, 1, true, true)
+		if err != nil {
+			log.WithContext(ctx).Errorf("Email Save Error %v", err)
+		}
 
-	if err != nil {
-		log.WithContext(ctx).Errorf("邮件内容解析失败! Error : %v \n", err)
+		send.Send(ctx, email)
+
+	} else {
+		// 收件
+
+		var dkimStatus, SPFStatus bool
+
+		// DKIM校验
+		dkimStatus = parsemail.Check(bytes.NewReader(emailData))
+
+		if err != nil {
+			log.WithContext(ctx).Errorf("邮件内容解析失败! Error : %v \n", err)
+		}
+
+		SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress)
+
+		saveEmail(ctx, email, 0, SPFStatus, dkimStatus)
+
+		log.WithContext(ctx).Debugf("开始执行插件!")
+
+		as2 := async.New(ctx)
+		for _, hook := range hooks.HookList {
+			if hook == nil {
+				continue
+			}
+			as2.WaitProcess(func(hk any) {
+				hk.(hooks.EmailHook).ReceiveParseAfter(email)
+			}, hook)
+		}
+		as2.Wait()
+
+		log.WithContext(ctx).Debugf("开始执行邮件规则!")
+		// 执行邮件规则
+		rs := rule.GetAllRules(ctx)
+		for _, r := range rs {
+			if rule.MatchRule(ctx, r, email) {
+				rule.DoRule(ctx, r, email)
+			}
+		}
 	}
 
-	SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress)
+	return nil
+}
 
+func saveEmail(ctx *context.Context, email *parsemail.Email, emailType int, SPFStatus, dkimStatus bool) error {
 	var dkimV, spfV int8
 	if dkimStatus {
 		dkimV = 1
@@ -75,35 +132,16 @@ func (s *Session) Data(r io.Reader) error {
 		log.WithContext(ctx).Infoln("垃圾邮件,拒信")
 		return nil
 	}
-	log.WithContext(ctx).Debugf("开始执行插件!")
 
-	as2 := async.New(ctx)
-	for _, hook := range hooks.HookList {
-		if hook == nil {
-			continue
-		}
-		as2.WaitProcess(func(hk any) {
-			hk.(hooks.EmailHook).ReceiveParseAfter(email)
-		}, hook)
-	}
-	as2.Wait()
-
-	log.WithContext(ctx).Debugf("开始执行邮件规则!")
-	// 执行邮件规则
-	rs := rule.GetAllRules(ctx)
-	for _, r := range rs {
-		if rule.MatchRule(ctx, r, email) {
-			rule.DoRule(ctx, r, email)
-		}
-	}
 	log.WithContext(ctx).Debugf("开始入库!")
 
 	if email == nil {
 		return nil
 	}
 
-	sql := "INSERT INTO email (send_date, subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,is_read,status,group_id) VALUES (?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
-	_, err = db.Instance.Exec(sql,
+	sql := "INSERT INTO email (type, send_date, subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,is_read,status,group_id) VALUES (?,?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+	_, err := db.Instance.Exec(sql,
+		emailType,
 		email.Date,
 		email.Subject,
 		json2string(email.ReplyTo),

+ 12 - 0
server/smtp_server/read_content_test.go

@@ -44,6 +44,18 @@ func testInit() {
 
 }
 
+func TestNuisanace(t *testing.T) {
+	testInit()
+
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+	}
+
+	data, _ := os.ReadFile("../docs/nuisance/demo.txt")
+	s.Data(bytes.NewReader(data))
+
+}
+
 func TestSession_Data(t *testing.T) {
 	testInit()
 	s := Session{

+ 88 - 3
server/smtp_server/smtp.go

@@ -2,10 +2,18 @@ package smtp_server
 
 import (
 	"crypto/tls"
+	"database/sql"
 	"github.com/emersion/go-smtp"
 	log "github.com/sirupsen/logrus"
 	"net"
 	"pmail/config"
+	"pmail/db"
+	"pmail/models"
+	"pmail/utils/context"
+	"pmail/utils/errors"
+	"pmail/utils/id"
+	"pmail/utils/password"
+	"strings"
 	"time"
 )
 
@@ -13,27 +21,70 @@ import (
 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
 }
 
-func (s *Session) AuthPlain(username, password string) error {
-	return nil
+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.Get(&user, db.WithContext(s.Ctx, "select * from user where account =? and password =?"),
+		username, encodePwd)
+	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
+
+		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) error {
+	log.WithContext(s.Ctx).Debugf("Rcpt Success %+v", to)
+
+	s.To = append(s.To, to)
 	return nil
 }
 
@@ -44,6 +95,36 @@ func (s *Session) Logout() error {
 }
 
 var instance *smtp.Server
+var instanceTls *smtp.Server
+
+func StartWithTLS() {
+	be := &Backend{}
+
+	instance = smtp.NewServer(be)
+
+	instance.Addr = ":465"
+	instance.Domain = config.Instance.Domain
+	instance.ReadTimeout = 10 * time.Second
+	instance.AuthDisabled = false
+	instance.WriteTimeout = 10 * time.Second
+	instance.MaxMessageBytes = 1024 * 1024
+	instance.MaxRecipients = 50
+	// force TLS for auth
+	instance.AllowInsecureAuth = false
+	// 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
+	instance.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
+
+	log.Println("Starting Smtp With SSL Server Port:", instance.Addr)
+	if err := instance.ListenAndServeTLS(); err != nil {
+		log.Fatal(err)
+	}
+}
 
 func Start() {
 	be := &Backend{}
@@ -53,6 +134,7 @@ func Start() {
 	instance.Addr = ":25"
 	instance.Domain = config.Instance.Domain
 	instance.ReadTimeout = 10 * time.Second
+	instance.AuthDisabled = false
 	instance.WriteTimeout = 10 * time.Second
 	instance.MaxMessageBytes = 1024 * 1024
 	instance.MaxRecipients = 50
@@ -67,7 +149,7 @@ func Start() {
 	// Configure the TLS support
 	instance.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
 
-	log.Println("Starting server at", instance.Addr)
+	log.Println("Starting Smtp Server Port:", instance.Addr)
 	if err := instance.ListenAndServe(); err != nil {
 		log.Fatal(err)
 	}
@@ -77,4 +159,7 @@ func Stop() {
 	if instance != nil {
 		instance.Close()
 	}
+	if instanceTls != nil {
+		instanceTls.Close()
+	}
 }

+ 1 - 1
server/utils/context/context.go

@@ -25,7 +25,7 @@ func (c *Context) SetValue(key string, value any) {
 
 }
 
-func (c Context) GetValue(key string) any {
+func (c *Context) GetValue(key string) any {
 	if c.values == nil {
 		return nil
 	}

+ 2 - 2
server/utils/password/encode.go

@@ -7,11 +7,11 @@ import (
 
 // Encode 对密码两次md5加盐
 func Encode(password string) string {
-	encodePwd := md5Encode(md5Encode(password+"pmail") + "pmail2023")
+	encodePwd := Md5Encode(Md5Encode(password+"pmail") + "pmail2023")
 	return encodePwd
 }
 
-func md5Encode(str string) string {
+func Md5Encode(str string) string {
 	h := md5.New()
 	h.Write([]byte(str))
 	return hex.EncodeToString(h.Sum(nil))

+ 14 - 49
server/utils/send/send.go

@@ -66,20 +66,11 @@ func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string) er
 		domain := domain
 		tos := tos
 		as.WaitProcess(func(p any) {
-
-			err := smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
-
+			err := smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
 			if err != nil {
-				log.WithContext(ctx).Warnf("SMTPS on 465 Send Error! Error:%+v", err)
-				// smtps发送失败,尝试smtp
-				err = smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
-				if err != nil {
-					log.WithContext(ctx).Warnf("SMTP Send Error! Error:%+v", err)
-				} else {
-					log.WithContext(ctx).Infof("SMTP Send Success !")
-				}
+				log.WithContext(ctx).Warnf("SMTP Send Error! Error:%+v", err)
 			} else {
-				log.WithContext(ctx).Infof("SMTPS on 465 Send Success !")
+				log.WithContext(ctx).Infof("SMTP Send Success !")
 			}
 
 			// 重新选取证书域名
@@ -88,21 +79,11 @@ func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string) er
 					if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
 						if hostnameErr.Certificate != nil {
 							certificateHostName := hostnameErr.Certificate.DNSNames
-
-							// 再使用smtps 465端口 尝试
-							err = smtp.SendMailWithTls(domainMatch(domain.domain, certificateHostName), domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
-
+							err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
 							if err != nil {
-								log.WithContext(ctx).Warnf("SMTPS on 465 Send Error! Error:%+v", err)
-								// smtps发送失败,尝试smtp
-								err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
-								if err != nil {
-									log.WithContext(ctx).Warnf("SMTP Send Error! Error:%+v", err)
-								} else {
-									log.WithContext(ctx).Infof("SMTP Send Success !")
-								}
+								log.WithContext(ctx).Warnf("SMTP Send Error! Error:%+v", err)
 							} else {
-								log.WithContext(ctx).Infof("SMTPS Send Success !")
+								log.WithContext(ctx).Infof("SMTP Send Success !")
 							}
 						}
 					}
@@ -128,7 +109,7 @@ func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string) er
 
 func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
 
-	b := e.BuildBytes(ctx)
+	b := e.BuildBytes(ctx, true)
 
 	var to []*parsemail.User
 	to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
@@ -170,19 +151,11 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
 		tos := tos
 		as.WaitProcess(func(p any) {
 
-			err := smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
-
+			err := smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
 			if err != nil {
-				log.WithContext(ctx).Warnf("SMTPS on 465 Send Error! Error:%+v", err)
-				// smtps发送失败,尝试smtp
-				err = smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
-				if err != nil {
-					log.WithContext(ctx).Warnf("SMTP Send Error! Error:%+v", err)
-				} else {
-					log.WithContext(ctx).Infof("SMTP Send Success !")
-				}
+				log.WithContext(ctx).Warnf("SMTP Send Error! Error:%+v", err)
 			} else {
-				log.WithContext(ctx).Infof("SMTPS Send Success !")
+				log.WithContext(ctx).Infof("SMTP Send Success !")
 			}
 
 			// 重新选取证书域名
@@ -191,20 +164,12 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
 					if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
 						if hostnameErr.Certificate != nil {
 							certificateHostName := hostnameErr.Certificate.DNSNames
-
-							err = smtp.SendMailWithTls(domainMatch(domain.domain, certificateHostName), domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
-
+							// smtps发送失败,尝试smtp
+							err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
 							if err != nil {
-								log.WithContext(ctx).Warnf("SMTPS on 465 Send Error! Error:%+v", err)
-								// smtps发送失败,尝试smtp
-								err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
-								if err != nil {
-									log.WithContext(ctx).Warnf("SMTP Send Error! Error:%+v", err)
-								} else {
-									log.WithContext(ctx).Infof("SMTP Send Success !")
-								}
+								log.WithContext(ctx).Warnf("SMTP Send Error! Error:%+v", err)
 							} else {
-								log.WithContext(ctx).Infof("SMTPS Send Success !")
+								log.WithContext(ctx).Infof("SMTP Send Success !")
 							}
 						}
 					}