parent
e194d6ecfd
commit
fcc863e8b8
@ -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/wechat_push/output/* ./plugins/
|
||||||
COPY --from=serverbuild /work/server/hooks/spam_block/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
|
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/wechat_push/output/* ./plugins/
|
||||||
COPY --from=serverbuild /work/hooks/spam_block/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
|
CMD /work/pmail
|
||||||
|
|||||||
@ -57,10 +57,10 @@ First go to [spamhaus](https://check.spamhaus.org/) and check your domain name a
|
|||||||
|
|
||||||
Or
|
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]
|
> [!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
|
## 3、Configuration
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ POP3 Port: 110/995(SSL)
|
|||||||
|
|
||||||
SMTP Server Address : smtp.[Your Domain]
|
SMTP Server Address : smtp.[Your Domain]
|
||||||
|
|
||||||
SMTP Port: 25/465(SSL)
|
SMTP Port: 25/465、587(SSL)
|
||||||
|
|
||||||
IMAP Server Address : imap.[Your Domain]
|
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]
|
> [!IMPORTANT]
|
||||||
> 如果你服务器开启了防火墙,你需要打开25、80、110、443、465、995、993端口
|
> 如果你服务器开启了防火墙,你需要打开25、80、110、443、465、587、995、993端口
|
||||||
|
|
||||||
## 3、配置
|
## 3、配置
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ POP3端口: 110/995(SSL)
|
|||||||
|
|
||||||
SMTP地址: smtp.[你的域名]
|
SMTP地址: smtp.[你的域名]
|
||||||
|
|
||||||
SMTP端口: 25/465(SSL)
|
SMTP端口: 25/465、587(SSL)
|
||||||
|
|
||||||
IMAP地址: imap.[Your Domain]
|
IMAP地址: imap.[Your Domain]
|
||||||
|
|
||||||
|
|||||||
@ -13,14 +13,14 @@
|
|||||||
<el-button size="small">
|
<el-button size="small">
|
||||||
{{ lang.move_btn }}
|
{{ lang.move_btn }}
|
||||||
<el-icon class="el-icon--right">
|
<el-icon class="el-icon--right">
|
||||||
<EpArrowDownBold />
|
<EpArrowDownBold/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item @click="move(group.id)" v-for="group in groupList" :key="group.id">{{
|
<el-dropdown-item @click="move(group.id,group.name)" v-for="group in groupList" :key="group.id">{{
|
||||||
group.name
|
group.name
|
||||||
}}
|
}}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
@ -28,8 +28,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="table">
|
<div id="table">
|
||||||
<el-table ref="taskTableDataRef" :data="data" :show-header="true" :border="false" @row-click="rowClick"
|
<el-table ref="taskTableDataRef" :data="data" :show-header="true" :border="false" @row-click="rowClick"
|
||||||
:row-style="rowStyle">
|
:row-style="rowStyle">
|
||||||
<el-table-column type="selection" width="30" />
|
<el-table-column type="selection" width="30"/>
|
||||||
<el-table-column prop="is_read" label="" width="50">
|
<el-table-column prop="is_read" label="" width="50">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div>
|
<div>
|
||||||
@ -64,7 +64,7 @@
|
|||||||
<el-table-column prop="title" :label="lang.to" width="150">
|
<el-table-column prop="title" :label="lang.to" width="150">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tooltip v-for="toInfo in scope.row.to" :key="toInfo" class="box-item" effect="dark"
|
<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-tag size="small" type="info">{{ toInfo.Name !== '' ? toInfo.Name : toInfo.EmailAddress }}</el-tag>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
@ -88,7 +88,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<div id="pagination">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -96,13 +96,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { EpArrowDownBold } from "vue-icons-plus/ep";
|
import {EpArrowDownBold} from "vue-icons-plus/ep";
|
||||||
import { RouterLink, useRouter } from 'vue-router'
|
import {RouterLink, useRouter} from 'vue-router'
|
||||||
import { ref, watch } from 'vue'
|
import {ref, watch} from 'vue'
|
||||||
import useGroupStore from '../stores/group'
|
import useGroupStore from '../stores/group'
|
||||||
import lang from '../i18n/i18n';
|
import lang from '../i18n/i18n';
|
||||||
import { http } from "@/utils/axios";
|
import {http} from "@/utils/axios";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import {ElMessage, ElMessageBox} from "element-plus";
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -122,7 +122,7 @@ watch(groupStore, async (newV) => {
|
|||||||
tag = '{"type":0,"status":-1}'
|
tag = '{"type":0,"status":-1}'
|
||||||
}
|
}
|
||||||
data.value = []
|
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
|
data.value = res.data.list
|
||||||
totalPage.value = res.data.total_page
|
totalPage.value = res.data.total_page
|
||||||
})
|
})
|
||||||
@ -133,7 +133,7 @@ const data = ref([])
|
|||||||
const totalPage = ref(0)
|
const totalPage = ref(0)
|
||||||
|
|
||||||
const updateList = function () {
|
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
|
data.value = res.data.list
|
||||||
totalPage.value = res.data.total_page
|
totalPage.value = res.data.total_page
|
||||||
})
|
})
|
||||||
@ -163,7 +163,7 @@ const markRead = function () {
|
|||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
http.post("/api/email/read", { "ids": ids }).then(res => {
|
http.post("/api/email/read", {"ids": ids}).then(res => {
|
||||||
if (res.errorNo === 0) {
|
if (res.errorNo === 0) {
|
||||||
updateList()
|
updateList()
|
||||||
} else {
|
} 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 rows = taskTableDataRef.value?.getSelectionRows()
|
||||||
let ids = []
|
let ids = []
|
||||||
rows.forEach(element => {
|
rows.forEach(element => {
|
||||||
@ -191,32 +191,32 @@ const move = function (group_id) {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
lang.move_email_confirm,
|
lang.move_email_confirm,
|
||||||
'Warning',
|
'Warning',
|
||||||
{
|
{
|
||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
cancelButtonText: 'Cancel',
|
cancelButtonText: 'Cancel',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
http.post("/api/email/move", { "group_id": group_id, "ids": ids }).then(res => {
|
http.post("/api/email/move", {"group_id": group_id, "group_name": group_name, "ids": ids}).then(res => {
|
||||||
if (res.errorNo === 0) {
|
if (res.errorNo === 0) {
|
||||||
updateList()
|
updateList()
|
||||||
ElMessage({
|
ElMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: 'Move completed',
|
message: 'Move completed',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: res.errorMsg,
|
message: res.errorMsg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,42 +236,42 @@ const del = function () {
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
lang.del_email_confirm,
|
lang.del_email_confirm,
|
||||||
'Warning',
|
'Warning',
|
||||||
{
|
{
|
||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
cancelButtonText: 'Cancel',
|
cancelButtonText: 'Cancel',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
http.post("/api/email/del", { "ids": ids, "forcedDel": groupTag.status === 3 }).then(res => {
|
http.post("/api/email/del", {"ids": ids, "forcedDel": groupTag.status === 3}).then(res => {
|
||||||
if (res.errorNo === 0) {
|
if (res.errorNo === 0) {
|
||||||
updateList()
|
updateList()
|
||||||
ElMessage({
|
ElMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: 'Delete completed',
|
message: 'Delete completed',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: res.errorMsg,
|
message: res.errorMsg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const rowStyle = function () {
|
const rowStyle = function () {
|
||||||
return { 'cursor': 'pointer' }
|
return {'cursor': 'pointer'}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageChange = function (p) {
|
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
|
data.value = res.data.list
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package email
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/Jinnrry/pmail/dto/response"
|
"github.com/Jinnrry/pmail/dto/response"
|
||||||
|
"github.com/Jinnrry/pmail/models"
|
||||||
"github.com/Jinnrry/pmail/services/group"
|
"github.com/Jinnrry/pmail/services/group"
|
||||||
"github.com/Jinnrry/pmail/utils/context"
|
"github.com/Jinnrry/pmail/utils/context"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@ -11,8 +12,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type moveRequest struct {
|
type moveRequest struct {
|
||||||
GroupId int `json:"group_id"`
|
GroupId int `json:"group_id"`
|
||||||
IDs []int `json:"ids"`
|
GroupName string `json:"group_name"`
|
||||||
|
IDs []int `json:"ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Move(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
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
|
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)
|
response.NewErrorResponse(response.ServerError, "Error", "").FPrint(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/Jinnrry/pmail/dto"
|
"github.com/Jinnrry/pmail/dto"
|
||||||
"github.com/Jinnrry/pmail/dto/response"
|
"github.com/Jinnrry/pmail/dto/response"
|
||||||
"github.com/Jinnrry/pmail/i18n"
|
"github.com/Jinnrry/pmail/i18n"
|
||||||
|
"github.com/Jinnrry/pmail/models"
|
||||||
"github.com/Jinnrry/pmail/services/group"
|
"github.com/Jinnrry/pmail/services/group"
|
||||||
"github.com/Jinnrry/pmail/utils/array"
|
"github.com/Jinnrry/pmail/utils/array"
|
||||||
"github.com/Jinnrry/pmail/utils/context"
|
"github.com/Jinnrry/pmail/utils/context"
|
||||||
@ -14,8 +15,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetUserGroupList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
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)
|
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) {
|
func GetUserGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ type Email struct {
|
|||||||
Attachments []*Attachment
|
Attachments []*Attachment
|
||||||
ReadReceipt []string
|
ReadReceipt []string
|
||||||
Date string
|
Date string
|
||||||
Status int // 0未发送,1已发送,2发送失败,3删除
|
Status int // 0未发送,1已发送,2发送失败,3删除,5广告邮件
|
||||||
MessageId int64
|
MessageId int64
|
||||||
Size int
|
Size int
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,23 @@ import (
|
|||||||
"os"
|
"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() {
|
func main() {
|
||||||
args := os.Args
|
args := os.Args
|
||||||
|
|
||||||
@ -49,7 +66,7 @@ func main() {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
content = tools.Trim(email.Text.String)
|
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 {
|
if err != nil {
|
||||||
fmt.Println(err)
|
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 {
|
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 (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"database/sql"
|
|
||||||
"github.com/Jinnrry/pmail/config"
|
"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"
|
"github.com/emersion/go-smtp"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"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 instance *smtp.Server
|
||||||
var instanceTls *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() {
|
func StartWithTLS() {
|
||||||
be := &Backend{}
|
be := &Backend{}
|
||||||
@ -185,4 +103,8 @@ func Stop() {
|
|||||||
if instanceTls != nil {
|
if instanceTls != nil {
|
||||||
instanceTls.Close()
|
instanceTls.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if instanceTlsNew != nil {
|
||||||
|
instanceTlsNew.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -200,8 +200,8 @@ func testCreateGroup(t *testing.T) {
|
|||||||
t.Error("CreateGroup Api Error!", data)
|
t.Error("CreateGroup Api Error!", data)
|
||||||
}
|
}
|
||||||
dt := data.Data.([]any)
|
dt := data.Data.([]any)
|
||||||
if len(dt) != 1 {
|
if len(dt) != 4 {
|
||||||
t.Error("Group List Is Empty!")
|
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')"`
|
EmailID int `xorm:"email_id not null index('idx_eid') index comment('信件id')"`
|
||||||
IsRead int8 `xorm:"is_read tinyint(1) comment('是否已读')" json:"is_read"`
|
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"`
|
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')"`
|
Created time.Time `xorm:"create datetime created index('idx_create_time')"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ func Init(serverVersion string) {
|
|||||||
// smtp server start
|
// smtp server start
|
||||||
go smtp_server.Start()
|
go smtp_server.Start()
|
||||||
go smtp_server.StartWithTLS()
|
go smtp_server.StartWithTLS()
|
||||||
|
go smtp_server.StartWithTLSNew()
|
||||||
// http server start
|
// http server start
|
||||||
go http_server.HttpsStart()
|
go http_server.HttpsStart()
|
||||||
go http_server.HttpStart()
|
go http_server.HttpStart()
|
||||||
|
|||||||
@ -51,7 +51,9 @@ func genSQL(ctx *context.Context, count bool, tagInfo dto.SearchTag, keyword str
|
|||||||
sql += " and ue.status =? "
|
sql += " and ue.status =? "
|
||||||
sqlParams = append(sqlParams, tagInfo.Status)
|
sqlParams = append(sqlParams, tagInfo.Status)
|
||||||
} else if tagInfo.Status == -1 {
|
} else if tagInfo.Status == -1 {
|
||||||
sql += " and ue.status = 0"
|
if tagInfo.Type != 1 {
|
||||||
|
sql += " and ue.status = 0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tagInfo.Type != -1 {
|
if tagInfo.Type != -1 {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/Jinnrry/pmail/config"
|
"github.com/Jinnrry/pmail/config"
|
||||||
"github.com/Jinnrry/pmail/dto/parsemail"
|
"github.com/Jinnrry/pmail/dto/parsemail"
|
||||||
"github.com/Jinnrry/pmail/models"
|
"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 {
|
func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string, user *models.User) error {
|
||||||
|
|
||||||
log.WithContext(ctx).Debugf("开始转发邮件")
|
log.WithContext(ctx).Debugf("开始转发邮件")
|
||||||
sender := fmt.Sprintf("%s@%s", user.Account, config.Instance.Domains[0])
|
|
||||||
|
|
||||||
b := e.ForwardBuildBytes(ctx, user)
|
b := e.ForwardBuildBytes(ctx, user)
|
||||||
|
|
||||||
@ -39,91 +37,9 @@ func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string, us
|
|||||||
{EmailAddress: forwardAddress},
|
{EmailAddress: forwardAddress},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按域名整理
|
err, _ := doSend(ctx, config.Instance.Domains[0], b, to, e.From.EmailAddress)
|
||||||
toByDomain := map[mxDomain][]*parsemail.User{}
|
|
||||||
for _, s := range to {
|
|
||||||
args := strings.Split(s.EmailAddress, "@")
|
|
||||||
if len(args) == 2 {
|
|
||||||
if args[1] == consts.TEST_DOMAIN {
|
|
||||||
// 测试使用
|
|
||||||
address := mxDomain{
|
|
||||||
domain: "localhost",
|
|
||||||
mxHost: "127.0.0.1",
|
|
||||||
}
|
|
||||||
toByDomain[address] = append(toByDomain[address], s)
|
|
||||||
} else {
|
|
||||||
//查询dns mx记录
|
|
||||||
mxInfo, err := net.LookupMX(args[1])
|
|
||||||
address := mxDomain{
|
|
||||||
domain: "smtp." + args[1],
|
|
||||||
mxHost: "smtp." + args[1],
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
|
|
||||||
}
|
|
||||||
if len(mxInfo) > 0 {
|
|
||||||
address = mxDomain{
|
|
||||||
domain: args[1],
|
|
||||||
mxHost: mxInfo[0].Host,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toByDomain[address] = append(toByDomain[address], s)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var errEmailAddress []string
|
return err
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
|
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
|
var to []*parsemail.User
|
||||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
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{}
|
toByDomain := map[mxDomain][]*parsemail.User{}
|
||||||
for _, s := range to {
|
for _, s := range to {
|
||||||
@ -181,32 +103,56 @@ func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
|
|||||||
tos := tos
|
tos := tos
|
||||||
as.WaitProcess(func(p any) {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// 使用其他方式发送
|
// 优先尝试25端口,starttls方式投递
|
||||||
if err != nil {
|
err := smtp.SendMail("", domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
|
||||||
// EOF 表示未知错误,此时降级为非tls连接发送(目前仅139邮箱有这个问题)
|
if err == nil {
|
||||||
if errors.Is(err, smtp.NoSupportSTARTTLSError) || err.Error() == "EOF" {
|
return
|
||||||
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)
|
var certificateErr *tls.CertificateVerificationError
|
||||||
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
|
if errors.As(err, &certificateErr) {
|
||||||
|
// 单测使用
|
||||||
|
var hostnameErr x509.HostnameError
|
||||||
|
if errors.As(certificateErr.Err, &hostnameErr) {
|
||||||
|
if hostnameErr.Certificate != nil {
|
||||||
|
certificateHostName := hostnameErr.Certificate.DNSNames
|
||||||
|
// 重新选取证书发送
|
||||||
|
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, from, fromDomain, buildAddress(tos), data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.WithContext(ctx).Infof("SMTP STARTTLS on 25 Send Error. %s", err.Error())
|
||||||
|
|
||||||
// 证书错误,从新选取证书发送
|
// 再试用587投递
|
||||||
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
|
err = smtp.SendMailWithTls("", domain.mxHost+":587", nil, from, fromDomain, buildAddress(tos), data)
|
||||||
// 单测使用
|
if err == nil {
|
||||||
if domain.domain == "localhost" {
|
return
|
||||||
err = smtp.SendMailUnsafe("", domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
|
}
|
||||||
} else if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
|
log.WithContext(ctx).Infof("SMTPS on 587 Send Error. %s", err.Error())
|
||||||
if hostnameErr.Certificate != nil {
|
|
||||||
certificateHostName := hostnameErr.Certificate.DNSNames
|
// 再次尝试465投递
|
||||||
// 重新选取证书发送
|
err = smtp.SendMailWithTls("", domain.mxHost+":465", nil, from, fromDomain, buildAddress(tos), data)
|
||||||
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, fromDomain, buildAddress(tos), b)
|
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 {
|
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 errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ",")), orgMap
|
||||||
}
|
}
|
||||||
return nil, orgMap
|
return nil, orgMap
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAddress(u []*parsemail.User) []string {
|
func buildAddress(u []*parsemail.User) []string {
|
||||||
|
|||||||
@ -69,6 +69,10 @@ func Dial(addr, fromDomain string) (*Client, error) {
|
|||||||
|
|
||||||
// with tls
|
// with tls
|
||||||
func DialTls(addr, domain, fromDomain string) (*Client, error) {
|
func DialTls(addr, domain, fromDomain string) (*Client, error) {
|
||||||
|
if domain == "" {
|
||||||
|
domain = fromDomain
|
||||||
|
}
|
||||||
|
|
||||||
// TLS config
|
// TLS config
|
||||||
tlsconfig := &tls.Config{
|
tlsconfig := &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user