mirror of
https://github.com/Jinnrry/PMail.git
synced 2025-09-26 19:31:21 +08:00
@@ -34,6 +34,6 @@ COPY --from=serverbuild /work/server/hooks/telegram_push/output/* ./plugins/
|
||||
COPY --from=serverbuild /work/server/hooks/wechat_push/output/* ./plugins/
|
||||
COPY --from=serverbuild /work/server/hooks/spam_block/output/* ./plugins/
|
||||
|
||||
EXPOSE 25 80 110 443 465 995 993
|
||||
EXPOSE 25 80 110 443 465 587 995 993
|
||||
|
||||
CMD /work/pmail
|
||||
|
@@ -28,6 +28,6 @@ COPY --from=serverbuild /work/hooks/telegram_push/output/* ./plugins/
|
||||
COPY --from=serverbuild /work/hooks/wechat_push/output/* ./plugins/
|
||||
COPY --from=serverbuild /work/hooks/spam_block/output/* ./plugins/
|
||||
|
||||
EXPOSE 25 80 110 443 465 995 993
|
||||
EXPOSE 25 80 110 443 465 587 995 993
|
||||
|
||||
CMD /work/pmail
|
||||
|
@@ -57,10 +57,10 @@ First go to [spamhaus](https://check.spamhaus.org/) and check your domain name a
|
||||
|
||||
Or
|
||||
|
||||
`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
|
||||
`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 587:587 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If your server has a firewall turned on, you need to open ports 25, 80, 110, 443, 465, 993, 995
|
||||
> If your server has a firewall turned on, you need to open ports 25, 80, 110, 443, 465,587, 993, 995
|
||||
|
||||
## 3、Configuration
|
||||
|
||||
@@ -102,7 +102,7 @@ POP3 Port: 110/995(SSL)
|
||||
|
||||
SMTP Server Address : smtp.[Your Domain]
|
||||
|
||||
SMTP Port: 25/465(SSL)
|
||||
SMTP Port: 25/465、587(SSL)
|
||||
|
||||
IMAP Server Address : imap.[Your Domain]
|
||||
|
||||
|
@@ -65,10 +65,10 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
|
||||
|
||||
或者
|
||||
|
||||
`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
|
||||
`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 587:587 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 如果你服务器开启了防火墙,你需要打开25、80、110、443、465、995、993端口
|
||||
> 如果你服务器开启了防火墙,你需要打开25、80、110、443、465、587、995、993端口
|
||||
|
||||
## 3、配置
|
||||
|
||||
@@ -107,7 +107,7 @@ POP3端口: 110/995(SSL)
|
||||
|
||||
SMTP地址: smtp.[你的域名]
|
||||
|
||||
SMTP端口: 25/465(SSL)
|
||||
SMTP端口: 25/465、587(SSL)
|
||||
|
||||
IMAP地址: imap.[Your Domain]
|
||||
|
||||
|
@@ -13,14 +13,14 @@
|
||||
<el-button size="small">
|
||||
{{ lang.move_btn }}
|
||||
<el-icon class="el-icon--right">
|
||||
<EpArrowDownBold />
|
||||
<EpArrowDownBold/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="move(group.id)" v-for="group in groupList" :key="group.id">{{
|
||||
group.name
|
||||
}}
|
||||
<el-dropdown-item @click="move(group.id,group.name)" v-for="group in groupList" :key="group.id">{{
|
||||
group.name
|
||||
}}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -28,8 +28,8 @@
|
||||
</div>
|
||||
<div id="table">
|
||||
<el-table ref="taskTableDataRef" :data="data" :show-header="true" :border="false" @row-click="rowClick"
|
||||
:row-style="rowStyle">
|
||||
<el-table-column type="selection" width="30" />
|
||||
:row-style="rowStyle">
|
||||
<el-table-column type="selection" width="30"/>
|
||||
<el-table-column prop="is_read" label="" width="50">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
@@ -64,7 +64,7 @@
|
||||
<el-table-column prop="title" :label="lang.to" width="150">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-for="toInfo in scope.row.to" :key="toInfo" class="box-item" effect="dark"
|
||||
:content="toInfo.EmailAddress" placement="top">
|
||||
:content="toInfo.EmailAddress" placement="top">
|
||||
<el-tag size="small" type="info">{{ toInfo.Name !== '' ? toInfo.Name : toInfo.EmailAddress }}</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -88,7 +88,7 @@
|
||||
</el-table>
|
||||
</div>
|
||||
<div id="pagination">
|
||||
<el-pagination background layout="prev, pager, next" :page-count="totalPage" @current-change="pageChange" />
|
||||
<el-pagination background layout="prev, pager, next" :page-count="totalPage" @current-change="pageChange"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -96,13 +96,13 @@
|
||||
|
||||
<script setup>
|
||||
|
||||
import { EpArrowDownBold } from "vue-icons-plus/ep";
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { ref, watch } from 'vue'
|
||||
import {EpArrowDownBold} from "vue-icons-plus/ep";
|
||||
import {RouterLink, useRouter} from 'vue-router'
|
||||
import {ref, watch} from 'vue'
|
||||
import useGroupStore from '../stores/group'
|
||||
import lang from '../i18n/i18n';
|
||||
import { http } from "@/utils/axios";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {http} from "@/utils/axios";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
@@ -122,7 +122,7 @@ watch(groupStore, async (newV) => {
|
||||
tag = '{"type":0,"status":-1}'
|
||||
}
|
||||
data.value = []
|
||||
http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
|
||||
http.post("/api/email/list", {tag: tag, page_size: 10}).then(res => {
|
||||
data.value = res.data.list
|
||||
totalPage.value = res.data.total_page
|
||||
})
|
||||
@@ -133,7 +133,7 @@ const data = ref([])
|
||||
const totalPage = ref(0)
|
||||
|
||||
const updateList = function () {
|
||||
http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
|
||||
http.post("/api/email/list", {tag: tag, page_size: 10}).then(res => {
|
||||
data.value = res.data.list
|
||||
totalPage.value = res.data.total_page
|
||||
})
|
||||
@@ -163,7 +163,7 @@ const markRead = function () {
|
||||
confirmButtonText: 'OK',
|
||||
})
|
||||
} else {
|
||||
http.post("/api/email/read", { "ids": ids }).then(res => {
|
||||
http.post("/api/email/read", {"ids": ids}).then(res => {
|
||||
if (res.errorNo === 0) {
|
||||
updateList()
|
||||
} else {
|
||||
@@ -177,7 +177,7 @@ const markRead = function () {
|
||||
}
|
||||
|
||||
|
||||
const move = function (group_id) {
|
||||
const move = function (group_id, group_name) {
|
||||
let rows = taskTableDataRef.value?.getSelectionRows()
|
||||
let ids = []
|
||||
rows.forEach(element => {
|
||||
@@ -191,32 +191,32 @@ const move = function (group_id) {
|
||||
})
|
||||
} else {
|
||||
ElMessageBox.confirm(
|
||||
lang.move_email_confirm,
|
||||
'Warning',
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
lang.move_email_confirm,
|
||||
'Warning',
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
http.post("/api/email/move", { "group_id": group_id, "ids": ids }).then(res => {
|
||||
if (res.errorNo === 0) {
|
||||
updateList()
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: 'Move completed',
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: res.errorMsg,
|
||||
})
|
||||
}
|
||||
.then(() => {
|
||||
http.post("/api/email/move", {"group_id": group_id, "group_name": group_name, "ids": ids}).then(res => {
|
||||
if (res.errorNo === 0) {
|
||||
updateList()
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: 'Move completed',
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: res.errorMsg,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,42 +236,42 @@ const del = function () {
|
||||
} else {
|
||||
|
||||
ElMessageBox.confirm(
|
||||
lang.del_email_confirm,
|
||||
'Warning',
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
lang.del_email_confirm,
|
||||
'Warning',
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
http.post("/api/email/del", { "ids": ids, "forcedDel": groupTag.status === 3 }).then(res => {
|
||||
if (res.errorNo === 0) {
|
||||
updateList()
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: 'Delete completed',
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: res.errorMsg,
|
||||
})
|
||||
}
|
||||
.then(() => {
|
||||
http.post("/api/email/del", {"ids": ids, "forcedDel": groupTag.status === 3}).then(res => {
|
||||
if (res.errorNo === 0) {
|
||||
updateList()
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: 'Delete completed',
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: res.errorMsg,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const rowStyle = function () {
|
||||
return { 'cursor': 'pointer' }
|
||||
return {'cursor': 'pointer'}
|
||||
}
|
||||
|
||||
const pageChange = function (p) {
|
||||
http.post("/api/email/list", { tag: tag, page_size: 10, current_page: p }).then(res => {
|
||||
http.post("/api/email/list", {tag: tag, page_size: 10, current_page: p}).then(res => {
|
||||
data.value = res.data.list
|
||||
})
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package email
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/Jinnrry/pmail/dto/response"
|
||||
"github.com/Jinnrry/pmail/models"
|
||||
"github.com/Jinnrry/pmail/services/group"
|
||||
"github.com/Jinnrry/pmail/utils/context"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -11,8 +12,9 @@ import (
|
||||
)
|
||||
|
||||
type moveRequest struct {
|
||||
GroupId int `json:"group_id"`
|
||||
IDs []int `json:"ids"`
|
||||
GroupId int `json:"group_id"`
|
||||
GroupName string `json:"group_name"`
|
||||
IDs []int `json:"ids"`
|
||||
}
|
||||
|
||||
func Move(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
@@ -31,7 +33,13 @@ func Move(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !group.MoveMailToGroup(ctx, reqData.IDs, reqData.GroupId) {
|
||||
if name, ok := models.GroupCodeToName[reqData.GroupId]; ok {
|
||||
err := group.Move2DefaultBox(ctx, reqData.IDs, name)
|
||||
if err != nil {
|
||||
response.NewErrorResponse(response.ServerError, "Error", err.Error()).FPrint(w)
|
||||
return
|
||||
}
|
||||
} else if !group.MoveMailToGroup(ctx, reqData.IDs, reqData.GroupId) {
|
||||
response.NewErrorResponse(response.ServerError, "Error", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/Jinnrry/pmail/dto"
|
||||
"github.com/Jinnrry/pmail/dto/response"
|
||||
"github.com/Jinnrry/pmail/i18n"
|
||||
"github.com/Jinnrry/pmail/models"
|
||||
"github.com/Jinnrry/pmail/services/group"
|
||||
"github.com/Jinnrry/pmail/utils/array"
|
||||
"github.com/Jinnrry/pmail/utils/context"
|
||||
@@ -14,8 +15,15 @@ import (
|
||||
)
|
||||
|
||||
func GetUserGroupList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
defaultGroup := []*models.Group{
|
||||
{models.INBOX, i18n.GetText(ctx.Lang, "inbox"), 0, 0, "/"},
|
||||
{models.Junk, i18n.GetText(ctx.Lang, "junk"), 0, 0, "/"},
|
||||
{models.Deleted, i18n.GetText(ctx.Lang, "deleted"), 0, 0, "/"},
|
||||
}
|
||||
|
||||
infos := group.GetGroupList(ctx)
|
||||
response.NewSuccessResponse(infos).FPrint(w)
|
||||
|
||||
response.NewSuccessResponse(append(defaultGroup, infos...)).FPrint(w)
|
||||
}
|
||||
|
||||
func GetUserGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
|
@@ -63,7 +63,7 @@ type Email struct {
|
||||
Attachments []*Attachment
|
||||
ReadReceipt []string
|
||||
Date string
|
||||
Status int // 0未发送,1已发送,2发送失败,3删除
|
||||
Status int // 0未发送,1已发送,2发送失败,3删除,5广告邮件
|
||||
MessageId int64
|
||||
Size int
|
||||
}
|
||||
|
@@ -10,6 +10,23 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func getType(emailId int) int {
|
||||
var ue models.UserEmail
|
||||
_, err := db.Instance.Table(&ue).Where("email_id = ?", emailId).Limit(1).Get(&ue)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
if ue.Status == 3 {
|
||||
return 2
|
||||
}
|
||||
|
||||
if ue.Status == 5 {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func main() {
|
||||
args := os.Args
|
||||
|
||||
@@ -49,7 +66,7 @@ func main() {
|
||||
if content == "" {
|
||||
content = tools.Trim(email.Text.String)
|
||||
}
|
||||
_, err = file.WriteString(fmt.Sprintf("0 \t%s %s\n", email.Subject, content))
|
||||
_, err = file.WriteString(fmt.Sprintf("%d \t%s %s\n", getType(email.Id), email.Subject, content))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
@@ -162,7 +162,11 @@ func (s *SpamBlock) ReceiveParseAfter(ctx *context.Context, email *parsemail.Ema
|
||||
}
|
||||
|
||||
if maxClass != 0 && maxScore > s.cfg.Threshold/100 {
|
||||
email.Status = 5
|
||||
if maxClass == 2 {
|
||||
email.Status = 3
|
||||
} else {
|
||||
email.Status = 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
117
server/listen/smtp_server/action.go
Normal file
117
server/listen/smtp_server/action.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package smtp_server
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/Jinnrry/pmail/db"
|
||||
"github.com/Jinnrry/pmail/models"
|
||||
"github.com/Jinnrry/pmail/utils/context"
|
||||
"github.com/Jinnrry/pmail/utils/errors"
|
||||
"github.com/Jinnrry/pmail/utils/id"
|
||||
"github.com/Jinnrry/pmail/utils/password"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The Backend implements SMTP server methods.
|
||||
type Backend struct{}
|
||||
|
||||
func (bkd *Backend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
|
||||
|
||||
remoteAddress := conn.Conn().RemoteAddr()
|
||||
ctx := &context.Context{}
|
||||
ctx.SetValue(context.LogID, id.GenLogID())
|
||||
log.WithContext(ctx).Debugf("新SMTP连接")
|
||||
|
||||
return &Session{
|
||||
RemoteAddress: remoteAddress,
|
||||
Ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// A Session is returned after EHLO.
|
||||
type Session struct {
|
||||
RemoteAddress net.Addr
|
||||
User string
|
||||
From string
|
||||
To []string
|
||||
Ctx *context.Context
|
||||
}
|
||||
|
||||
// AuthMechanisms returns a slice of available auth mechanisms
|
||||
// supported in this example.
|
||||
func (s *Session) AuthMechanisms() []string {
|
||||
return []string{sasl.Plain, sasl.Login}
|
||||
}
|
||||
|
||||
// Auth is the handler for supported authenticators.
|
||||
func (s *Session) Auth(mech string) (sasl.Server, error) {
|
||||
log.WithContext(s.Ctx).Debugf("Auth :%s", mech)
|
||||
if mech == sasl.Plain {
|
||||
return sasl.NewPlainServer(func(identity, username, password string) error {
|
||||
return s.AuthPlain(username, password)
|
||||
}), nil
|
||||
}
|
||||
|
||||
if mech == sasl.Login {
|
||||
return NewLoginServer(func(username, password string) error {
|
||||
return s.AuthPlain(username, password)
|
||||
}), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Auth Not Supported")
|
||||
}
|
||||
|
||||
func (s *Session) AuthPlain(username, pwd string) error {
|
||||
log.WithContext(s.Ctx).Debugf("Auth %s %s", username, pwd)
|
||||
|
||||
s.User = username
|
||||
|
||||
var user models.User
|
||||
|
||||
encodePwd := password.Encode(pwd)
|
||||
|
||||
infos := strings.Split(username, "@")
|
||||
if len(infos) > 1 {
|
||||
username = infos[0]
|
||||
}
|
||||
|
||||
_, err := db.Instance.Where("account =? and password =? and disabled=0", username, encodePwd).Get(&user)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Errorf("%+v", err)
|
||||
}
|
||||
|
||||
if user.ID > 0 {
|
||||
s.Ctx.UserAccount = user.Account
|
||||
s.Ctx.UserID = user.ID
|
||||
s.Ctx.UserName = user.Name
|
||||
s.Ctx.IsAdmin = user.IsAdmin == 1
|
||||
|
||||
log.WithContext(s.Ctx).Debugf("Auth Success %+v", user)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.WithContext(s.Ctx).Debugf("登陆错误%s %s", username, pwd)
|
||||
return errors.New("password error")
|
||||
}
|
||||
|
||||
func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
|
||||
log.WithContext(s.Ctx).Debugf("Mail Success %+v %+v", from, opts)
|
||||
s.From = from
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
|
||||
log.WithContext(s.Ctx).Debugf("Rcpt Success %+v", to)
|
||||
|
||||
s.To = append(s.To, to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Reset() {}
|
||||
|
||||
func (s *Session) Logout() error {
|
||||
return nil
|
||||
}
|
@@ -2,125 +2,43 @@ package smtp_server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"github.com/Jinnrry/pmail/config"
|
||||
"github.com/Jinnrry/pmail/db"
|
||||
"github.com/Jinnrry/pmail/models"
|
||||
"github.com/Jinnrry/pmail/utils/context"
|
||||
"github.com/Jinnrry/pmail/utils/errors"
|
||||
"github.com/Jinnrry/pmail/utils/id"
|
||||
"github.com/Jinnrry/pmail/utils/password"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The Backend implements SMTP server methods.
|
||||
type Backend struct{}
|
||||
|
||||
func (bkd *Backend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
|
||||
|
||||
remoteAddress := conn.Conn().RemoteAddr()
|
||||
ctx := &context.Context{}
|
||||
ctx.SetValue(context.LogID, id.GenLogID())
|
||||
log.WithContext(ctx).Debugf("新SMTP连接")
|
||||
|
||||
return &Session{
|
||||
RemoteAddress: remoteAddress,
|
||||
Ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// A Session is returned after EHLO.
|
||||
type Session struct {
|
||||
RemoteAddress net.Addr
|
||||
User string
|
||||
From string
|
||||
To []string
|
||||
Ctx *context.Context
|
||||
}
|
||||
|
||||
// AuthMechanisms returns a slice of available auth mechanisms
|
||||
// supported in this example.
|
||||
func (s *Session) AuthMechanisms() []string {
|
||||
return []string{sasl.Plain, sasl.Login}
|
||||
}
|
||||
|
||||
// Auth is the handler for supported authenticators.
|
||||
func (s *Session) Auth(mech string) (sasl.Server, error) {
|
||||
log.WithContext(s.Ctx).Debugf("Auth :%s", mech)
|
||||
if mech == sasl.Plain {
|
||||
return sasl.NewPlainServer(func(identity, username, password string) error {
|
||||
return s.AuthPlain(username, password)
|
||||
}), nil
|
||||
}
|
||||
|
||||
if mech == sasl.Login {
|
||||
return NewLoginServer(func(username, password string) error {
|
||||
return s.AuthPlain(username, password)
|
||||
}), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Auth Not Supported")
|
||||
}
|
||||
|
||||
func (s *Session) AuthPlain(username, pwd string) error {
|
||||
log.WithContext(s.Ctx).Debugf("Auth %s %s", username, pwd)
|
||||
|
||||
s.User = username
|
||||
|
||||
var user models.User
|
||||
|
||||
encodePwd := password.Encode(pwd)
|
||||
|
||||
infos := strings.Split(username, "@")
|
||||
if len(infos) > 1 {
|
||||
username = infos[0]
|
||||
}
|
||||
|
||||
_, err := db.Instance.Where("account =? and password =? and disabled=0", username, encodePwd).Get(&user)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Errorf("%+v", err)
|
||||
}
|
||||
|
||||
if user.ID > 0 {
|
||||
s.Ctx.UserAccount = user.Account
|
||||
s.Ctx.UserID = user.ID
|
||||
s.Ctx.UserName = user.Name
|
||||
s.Ctx.IsAdmin = user.IsAdmin == 1
|
||||
|
||||
log.WithContext(s.Ctx).Debugf("Auth Success %+v", user)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.WithContext(s.Ctx).Debugf("登陆错误%s %s", username, pwd)
|
||||
return errors.New("password error")
|
||||
}
|
||||
|
||||
func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
|
||||
log.WithContext(s.Ctx).Debugf("Mail Success %+v %+v", from, opts)
|
||||
s.From = from
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
|
||||
log.WithContext(s.Ctx).Debugf("Rcpt Success %+v", to)
|
||||
|
||||
s.To = append(s.To, to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Reset() {}
|
||||
|
||||
func (s *Session) Logout() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var instance *smtp.Server
|
||||
var instanceTls *smtp.Server
|
||||
var instanceTlsNew *smtp.Server
|
||||
|
||||
func StartWithTLSNew() {
|
||||
be := &Backend{}
|
||||
|
||||
instanceTlsNew = smtp.NewServer(be)
|
||||
|
||||
instanceTlsNew.Addr = ":587"
|
||||
instanceTlsNew.Domain = config.Instance.Domain
|
||||
instanceTlsNew.ReadTimeout = 10 * time.Second
|
||||
instanceTlsNew.WriteTimeout = 10 * time.Second
|
||||
instanceTlsNew.MaxMessageBytes = 1024 * 1024 * 30
|
||||
instanceTlsNew.MaxRecipients = 50
|
||||
// force TLS for auth
|
||||
instanceTlsNew.AllowInsecureAuth = true
|
||||
// Load the certificate and key
|
||||
cer, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
// Configure the TLS support
|
||||
instanceTlsNew.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
|
||||
|
||||
log.Println("Starting Smtp With SSL Server Port:", instanceTlsNew.Addr)
|
||||
if err := instanceTlsNew.ListenAndServeTLS(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func StartWithTLS() {
|
||||
be := &Backend{}
|
||||
@@ -185,4 +103,8 @@ func Stop() {
|
||||
if instanceTls != nil {
|
||||
instanceTls.Close()
|
||||
}
|
||||
|
||||
if instanceTlsNew != nil {
|
||||
instanceTlsNew.Close()
|
||||
}
|
||||
}
|
||||
|
@@ -200,8 +200,8 @@ func testCreateGroup(t *testing.T) {
|
||||
t.Error("CreateGroup Api Error!", data)
|
||||
}
|
||||
dt := data.Data.([]any)
|
||||
if len(dt) != 1 {
|
||||
t.Error("Group List Is Empty!")
|
||||
if len(dt) != 4 {
|
||||
t.Errorf("Group List Check Error!,response: %+v", data)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,7 @@ type UserEmail struct {
|
||||
EmailID int `xorm:"email_id not null index('idx_eid') index comment('信件id')"`
|
||||
IsRead int8 `xorm:"is_read tinyint(1) comment('是否已读')" json:"is_read"`
|
||||
GroupId int `xorm:"group_id int notnull default(0) comment('分组id')'" json:"group_id"`
|
||||
Status int8 `xorm:"status tinyint(4) notnull default(0) comment('0未发送或收件,1已发送,2发送失败,3删除')" json:"status"` // 0未发送或收件,1已发送,2发送失败 3删除 4草稿箱(Drafts) 5骚扰邮件(Junk)
|
||||
Status int8 `xorm:"status tinyint(4) notnull default(0) comment('0未发送或收件,1已发送,2发送失败,3删除 4草稿 5广告')" json:"status"` // 0未发送或收件,1已发送,2发送失败 3删除 4草稿箱(Drafts) 5骚扰邮件(Junk)
|
||||
Created time.Time `xorm:"create datetime created index('idx_create_time')"`
|
||||
}
|
||||
|
||||
|
@@ -43,6 +43,7 @@ func Init(serverVersion string) {
|
||||
// smtp server start
|
||||
go smtp_server.Start()
|
||||
go smtp_server.StartWithTLS()
|
||||
go smtp_server.StartWithTLSNew()
|
||||
// http server start
|
||||
go http_server.HttpsStart()
|
||||
go http_server.HttpStart()
|
||||
|
@@ -51,7 +51,9 @@ func genSQL(ctx *context.Context, count bool, tagInfo dto.SearchTag, keyword str
|
||||
sql += " and ue.status =? "
|
||||
sqlParams = append(sqlParams, tagInfo.Status)
|
||||
} else if tagInfo.Status == -1 {
|
||||
sql += " and ue.status = 0"
|
||||
if tagInfo.Type != 1 {
|
||||
sql += " and ue.status = 0"
|
||||
}
|
||||
}
|
||||
|
||||
if tagInfo.Type != -1 {
|
||||
|
@@ -4,7 +4,6 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Jinnrry/pmail/config"
|
||||
"github.com/Jinnrry/pmail/dto/parsemail"
|
||||
"github.com/Jinnrry/pmail/models"
|
||||
@@ -28,7 +27,6 @@ type mxDomain struct {
|
||||
func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string, user *models.User) error {
|
||||
|
||||
log.WithContext(ctx).Debugf("开始转发邮件")
|
||||
sender := fmt.Sprintf("%s@%s", user.Account, config.Instance.Domains[0])
|
||||
|
||||
b := e.ForwardBuildBytes(ctx, user)
|
||||
|
||||
@@ -39,91 +37,9 @@ func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string, us
|
||||
{EmailAddress: forwardAddress},
|
||||
}
|
||||
|
||||
// 按域名整理
|
||||
toByDomain := map[mxDomain][]*parsemail.User{}
|
||||
for _, s := range to {
|
||||
args := strings.Split(s.EmailAddress, "@")
|
||||
if len(args) == 2 {
|
||||
if args[1] == consts.TEST_DOMAIN {
|
||||
// 测试使用
|
||||
address := mxDomain{
|
||||
domain: "localhost",
|
||||
mxHost: "127.0.0.1",
|
||||
}
|
||||
toByDomain[address] = append(toByDomain[address], s)
|
||||
} else {
|
||||
//查询dns mx记录
|
||||
mxInfo, err := net.LookupMX(args[1])
|
||||
address := mxDomain{
|
||||
domain: "smtp." + args[1],
|
||||
mxHost: "smtp." + args[1],
|
||||
}
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
|
||||
}
|
||||
if len(mxInfo) > 0 {
|
||||
address = mxDomain{
|
||||
domain: args[1],
|
||||
mxHost: mxInfo[0].Host,
|
||||
}
|
||||
}
|
||||
toByDomain[address] = append(toByDomain[address], s)
|
||||
}
|
||||
} else {
|
||||
log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
|
||||
continue
|
||||
}
|
||||
}
|
||||
err, _ := doSend(ctx, config.Instance.Domains[0], b, to, e.From.EmailAddress)
|
||||
|
||||
var errEmailAddress []string
|
||||
|
||||
as := async.New(ctx)
|
||||
for domain, tos := range toByDomain {
|
||||
domain := domain
|
||||
tos := tos
|
||||
as.WaitProcess(func(p any) {
|
||||
err := smtp.SendMail("", domain.mxHost+":25", nil, sender, config.Instance.Domains[0], buildAddress(tos), b)
|
||||
|
||||
// 使用其他方式发送
|
||||
if err != nil {
|
||||
// EOF 表示未知错误,此时降级为非tls连接发送(目前仅139邮箱有这个问题)
|
||||
if errors.Is(err, smtp.NoSupportSTARTTLSError) || err.Error() == "EOF" {
|
||||
err = smtp.SendMailWithTls("", domain.mxHost+":465", nil, sender, config.Instance.Domains[0], buildAddress(tos), b)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Warnf("Unsafe! %s Server Not Support SMTPS & STARTTLS", domain.domain)
|
||||
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, sender, config.Instance.Domains[0], buildAddress(tos), b)
|
||||
}
|
||||
}
|
||||
|
||||
// 证书错误,从新选取证书发送
|
||||
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
|
||||
// 单测使用
|
||||
if domain.domain == "localhost" {
|
||||
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, sender, config.Instance.Domains[0], buildAddress(tos), b)
|
||||
} else if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
|
||||
if hostnameErr.Certificate != nil {
|
||||
certificateHostName := hostnameErr.Certificate.DNSNames
|
||||
// 重新选取证书发送
|
||||
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, sender, config.Instance.Domains[0], buildAddress(tos), b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
|
||||
for _, user := range tos {
|
||||
errEmailAddress = append(errEmailAddress, user.EmailAddress)
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
}
|
||||
as.Wait()
|
||||
|
||||
if len(errEmailAddress) > 0 {
|
||||
return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ","))
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
|
||||
@@ -135,6 +51,12 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
|
||||
var to []*parsemail.User
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
|
||||
return doSend(ctx, fromDomain, b, to, e.From.EmailAddress)
|
||||
|
||||
}
|
||||
|
||||
func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemail.User, from string) (error, map[string]error) {
|
||||
|
||||
// 按域名整理
|
||||
toByDomain := map[mxDomain][]*parsemail.User{}
|
||||
for _, s := range to {
|
||||
@@ -181,32 +103,56 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
|
||||
tos := tos
|
||||
as.WaitProcess(func(p any) {
|
||||
|
||||
err := smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
|
||||
if domain.domain == "localhost" {
|
||||
err := smtp.SendMailUnsafe("", domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("send error %s", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 使用其他方式发送
|
||||
if err != nil {
|
||||
// EOF 表示未知错误,此时降级为非tls连接发送(目前仅139邮箱有这个问题)
|
||||
if errors.Is(err, smtp.NoSupportSTARTTLSError) || err.Error() == "EOF" {
|
||||
err = smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Warnf("Unsafe! %s Server Not Support SMTPS & STARTTLS", domain.domain)
|
||||
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
|
||||
// 优先尝试25端口,starttls方式投递
|
||||
err := smtp.SendMail("", domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
// 证书错误,从新选取证书发送
|
||||
var certificateErr *tls.CertificateVerificationError
|
||||
if errors.As(err, &certificateErr) {
|
||||
// 单测使用
|
||||
var hostnameErr x509.HostnameError
|
||||
if errors.As(certificateErr.Err, &hostnameErr) {
|
||||
if hostnameErr.Certificate != nil {
|
||||
certificateHostName := hostnameErr.Certificate.DNSNames
|
||||
// 重新选取证书发送
|
||||
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.WithContext(ctx).Infof("SMTP STARTTLS on 25 Send Error. %s", err.Error())
|
||||
|
||||
// 证书错误,从新选取证书发送
|
||||
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
|
||||
// 单测使用
|
||||
if domain.domain == "localhost" {
|
||||
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
|
||||
} else if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
|
||||
if hostnameErr.Certificate != nil {
|
||||
certificateHostName := hostnameErr.Certificate.DNSNames
|
||||
// 重新选取证书发送
|
||||
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 再试用587投递
|
||||
err = smtp.SendMailWithTls("", domain.mxHost+":587", nil, from, fromDomain, buildAddress(tos), data)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.WithContext(ctx).Infof("SMTPS on 587 Send Error. %s", err.Error())
|
||||
|
||||
// 再次尝试465投递
|
||||
err = smtp.SendMailWithTls("", domain.mxHost+":465", nil, from, fromDomain, buildAddress(tos), data)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.WithContext(ctx).Infof("SMTPS on 465 Send Error. %s", err.Error())
|
||||
|
||||
// 最后尝试非安全方式投递
|
||||
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
|
||||
if err == nil {
|
||||
log.WithContext(ctx).Warnf("Send By Unsafe SMTP")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -235,7 +181,6 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
|
||||
return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ",")), orgMap
|
||||
}
|
||||
return nil, orgMap
|
||||
|
||||
}
|
||||
|
||||
func buildAddress(u []*parsemail.User) []string {
|
||||
|
@@ -69,6 +69,10 @@ func Dial(addr, fromDomain string) (*Client, error) {
|
||||
|
||||
// with tls
|
||||
func DialTls(addr, domain, fromDomain string) (*Client, error) {
|
||||
if domain == "" {
|
||||
domain = fromDomain
|
||||
}
|
||||
|
||||
// TLS config
|
||||
tlsconfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
|
Reference in New Issue
Block a user