diff --git a/.github/workflows/docker_build_pre.yml b/.github/workflows/docker_build_pre.yml new file mode 100644 index 0000000..f37dc0e --- /dev/null +++ b/.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 }} diff --git a/README.md b/README.md index 2de3c53..bebcdd4 100644 --- a/README.md +++ b/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 diff --git a/README_CN.md b/README_CN.md index f07564a..0debc65 100644 --- a/README_CN.md +++ b/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) + # 参与开发 ## 项目架构 diff --git a/docs/nuisance/demo.txt b/docs/nuisance/demo.txt new file mode 100644 index 0000000..a9f3f6f --- /dev/null +++ b/docs/nuisance/demo.txt @@ -0,0 +1,152 @@ +Content-Type: multipart/related; boundary="===============7312606306475594684==" +MIME-Version: 1.0 +From: Fassberg Construction#Panel +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: +Content-Disposition: inline; filename="=?utf-8?q?logo=5F874H48=2Epng?=" + +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 + +--===============7312606306475594684==-- diff --git a/server/config/config.go b/server/config/config.go index 5fc0787..ca23e86 100644 --- a/server/config/config.go +++ b/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{} diff --git a/server/cron_server/ssl_update.go b/server/cron_server/ssl_update.go index f4de128..aefd186 100644 --- a/server/cron_server/ssl_update.go +++ b/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 { - if config.Instance != nil && config.Instance.IsInit { - days, err := ssl.CheckSSLCrtInfo() + 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 && 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) diff --git a/server/dto/parsemail/email.go b/server/dto/parsemail/email.go index a642e6d..8439d93 100644 --- a/server/dto/parsemail/email.go +++ b/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() } diff --git a/server/dto/parsemail/email_decode_test.go b/server/dto/parsemail/email_decode_test.go index bdce91e..0c0f2f9 100644 --- a/server/dto/parsemail/email_decode_test.go +++ b/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))) } diff --git a/server/dto/parsemail/email_test.go b/server/dto/parsemail/email_test.go index a068b3e..bdf82cc 100644 --- a/server/dto/parsemail/email_test.go +++ b/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, "

Hello World!

This is an HTML part.

") + 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)) +} diff --git a/server/go.mod b/server/go.mod index 7a22e8a..5934e3d 100644 --- a/server/go.mod +++ b/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 ) diff --git a/server/go.sum b/server/go.sum index 611cc21..1a8f39e 100644 --- a/server/go.sum +++ b/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= diff --git a/server/main.go b/server/main.go index 12f6323..458f65b 100644 --- a/server/main.go +++ b/server/main.go @@ -81,7 +81,7 @@ func main() { log.Infoln("***************************************************") // 定时任务启动 - go cron_server.Start() + cron_server.Start() // 核心服务启动 res_init.Init() diff --git a/server/models/email.go b/server/models/email.go index 4bf93dc..ac3d620 100644 --- a/server/models/email.go +++ b/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"), + } + +} diff --git a/server/pop3_server/action.go b/server/pop3_server/action.go new file mode 100644 index 0000000..edd86c0 --- /dev/null +++ b/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 +} diff --git a/server/pop3_server/pop3server.go b/server/pop3_server/pop3server.go new file mode 100644 index 0000000..8ff9644 --- /dev/null +++ b/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() +} diff --git a/server/res_init/init.go b/server/res_init/init.go index 68f7826..71144f7 100644 --- a/server/res_init/init.go +++ b/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() } } diff --git a/server/services/setup/ssl/ssl.go b/server/services/setup/ssl/ssl.go index b4fef3e..848b383 100644 --- a/server/services/setup/ssl/ssl.go +++ b/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 } diff --git a/server/smtp_server/read_content.go b/server/smtp_server/read_content.go index 1f86dd2..0c5719e 100644 --- a/server/smtp_server/read_content.go +++ b/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)) - - email := parsemail.NewEmailFromReader(bytes.NewReader(emailData)) - - if err != nil { - log.WithContext(ctx).Errorf("邮件内容解析失败! Error : %v \n", err) + 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 + } } - SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress) + // 判断是收信还是转发 + 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) + } + 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) + } + } + } + + 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), diff --git a/server/smtp_server/read_content_test.go b/server/smtp_server/read_content_test.go index 2554c2c..52a1f99 100644 --- a/server/smtp_server/read_content_test.go +++ b/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{ diff --git a/server/smtp_server/smtp.go b/server/smtp_server/smtp.go index c313709..b1f8170 100644 --- a/server/smtp_server/smtp.go +++ b/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,15 +95,17 @@ func (s *Session) Logout() error { } var instance *smtp.Server +var instanceTls *smtp.Server -func Start() { +func StartWithTLS() { be := &Backend{} instance = smtp.NewServer(be) - instance.Addr = ":25" + 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 @@ -67,7 +120,36 @@ 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 With SSL Server Port:", instance.Addr) + if err := instance.ListenAndServeTLS(); err != nil { + log.Fatal(err) + } +} + +func Start() { + be := &Backend{} + + instance = smtp.NewServer(be) + + 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 + // 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 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() + } } diff --git a/server/utils/context/context.go b/server/utils/context/context.go index da84628..c2d898c 100644 --- a/server/utils/context/context.go +++ b/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 } diff --git a/server/utils/password/encode.go b/server/utils/password/encode.go index 3b5b567..01e86bb 100644 --- a/server/utils/password/encode.go +++ b/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)) diff --git a/server/utils/send/send.go b/server/utils/send/send.go index 6fb2761..948d3d2 100644 --- a/server/utils/send/send.go +++ b/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 !") } } }