This commit is contained in:
jinnrry 2023-08-27 19:42:00 +08:00
parent d18a665c79
commit 5ef5057032
37 changed files with 773 additions and 59 deletions

View File

@ -0,0 +1,97 @@
<template>
<div id="main">
<el-tree :expand-on-click-node="false" :data="data" :props="defaultProps" :defaultExpandAll="true" :class="node">
<template #default="{ node, data }">
<div>
<span v-if="data.id != -1"> {{ data.label }}</span>
<el-input v-if="data.id == -1" v-model="data.label" @blur="onInputBlur(data)"></el-input>
<el-button v-if="data.id != 0" @click="del(node, data)" size="small" type="danger" circle> -
</el-button>
<el-button v-if="data.id != 0" @click="add(data)" size="small" type="primary" circle> + </el-button>
</div>
</template>
</el-tree>
<el-button @click="addRoot">{{ lang.add_group }}</el-button>
</div>
</template>
<script setup>
import $http from "../http/http";
import { reactive, ref } from 'vue'
import lang from '../i18n/i18n';
const data = reactive([])
$http.get('/api/group').then(res => {
data.push(...res.data)
})
const del = function (node, data) {
if (data.id != -1) {
$http.post("/api/group/del", { "id": data.id }).then(res => {
if (res.errorNo != 0) {
ElMessage({
message: res.errorMsg,
type: 'error',
})
} else {
const pc = node.parent.childNodes
for (let i = 0; i < pc.length; i++) {
if (pc[i].id == node.id) {
pc.splice(i, 1)
return
}
}
}
})
} else {
const pc = node.parent.childNodes
for (let i = 0; i < pc.length; i++) {
if (pc[i].id == node.id) {
pc.splice(i, 1)
return
}
}
}
}
const add = function (item) {
if (item.children == null) {
item.children = []
}
item.children.push({
"children": [],
"label": "",
"id": "-1",
"parent_id": item.id
})
}
const addRoot = function () {
data.push({
"children": [],
"label": "",
"id": "-1",
"parent_id": 0
})
}
const onInputBlur = function (item) {
if (item.label != "") {
$http.post("/api/group/add", { "name": item.label, "parent_id": item.parent_id }).then(res => {
if (res.errorNo != 0) {
ElMessage({
message: res.errorMsg,
type: 'error',
})
} else {
$http.get('/api/group').then(res => {
data.splice(0, data.length)
data.push(...res.data)
})
}
})
}
}
</script>

View File

@ -61,4 +61,10 @@ const handleNodeClick = function (data) {
.el-tree {
background-color: #F1F1F1;
}
.add_group{
font-size: 14px;
text-align: left;
padding-left: 15px;
}
</style>

View File

@ -8,11 +8,15 @@
<Setting style="color:#FFFFFF" />
</el-icon>
</div>
<el-drawer v-model="openSettings" :title="lang.settings">
<el-drawer v-model="openSettings" size="80%" :title="lang.settings">
<el-tabs tab-position="left" >
<el-tab-pane :label="lang.security">
<SecuritySettings/>
</el-tab-pane>
<el-tab-pane :label="lang.group_settings">
<GroupSettings/>
</el-tab-pane>
</el-tabs>
</el-drawer>
@ -25,6 +29,7 @@ import { ref } from 'vue'
import { ElMessageBox } from 'element-plus'
import SecuritySettings from '@/components/SecuritySettings.vue'
import lang from '../i18n/i18n';
import GroupSettings from './GroupSettings.vue';
const openSettings = ref(false)

View File

@ -58,6 +58,13 @@ var lang = {
"ssl_manuallyf": "Manually configure an SSL certificate",
"ssl_key_path": "ssl key file path",
"ssl_crt_path": "ssl crt file path",
"group_settings": "Group",
"add_group": "Add Group",
"del_email_confirm": "Are you sure you want to delete them?",
"move_email_confirm": "Are you sure you want to move them?",
"del_btn": "Delete",
"move_btn": "Move",
"read_btn": "Readed"
};
@ -122,6 +129,13 @@ var zhCN = {
"ssl_manuallyf": "手动配置SSL证书",
"ssl_key_path": "ssl key文件位置",
"ssl_crt_path": "ssl crt文件位置",
"group_settings": "分组",
"add_group": "新建分组",
"del_email_confirm": "你确定要删除吗?",
"move_email_confirm": "你确定要移动这些邮件吗?",
"del_btn": "删除",
"move_btn": "移动",
"read_btn": "已读"
}
switch (navigator.language) {

View File

@ -4,11 +4,27 @@
<div id="action">
<RouterLink to="/editer">+{{ lang.compose }}</RouterLink>
</div>
<!-- <div id="action">全部标记为已读</div> -->
</div>
<div id="title">{{ groupStore.name }}</div>
<div id="action">
<el-button @click="del" size="small">{{ lang.del_btn }}</el-button>
<el-button @click="markRead" size="small">{{ lang.read_btn }}</el-button>
<el-dropdown style="margin-left: 12px;">
<el-button size="small">
{{ lang.move_btn }}
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="move(group.id)" v-for="group in groupList">{{ group.name
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div id="table">
<el-table :data="data" :show-header="true" :border="false" @row-click="rowClick" :row-style="rowStyle">
<el-table ref="taskTableDataRef" @selection-change="selectionLineChange" :data="data" :show-header="true"
:border="false" @row-click="rowClick" :row-style="rowStyle">
<el-table-column type="selection" width="30" />
<el-table-column prop="title" label="" width="50">
<template #default="scope">
@ -49,7 +65,7 @@
</el-table>
</div>
<div id="pagination">
<el-pagination background layout="prev, pager, next" :page-count="totalPage" />
<el-pagination background layout="prev, pager, next" :page-count="totalPage" @current-change="pageChange" />
</div>
</div>
</template>
@ -58,19 +74,18 @@
<script setup>
import $http from "../http/http";
import { ArrowDown } from '@element-plus/icons-vue'
import { RouterLink } from 'vue-router'
import { reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import router from "@/router"; //
import useGroupStore from '../stores/group'
import lang from '../i18n/i18n';
const groupStore = useGroupStore()
const route = useRoute()
const groupList = ref([])
const taskTableDataRef = ref(null)
let tag = groupStore.tag;
@ -96,29 +111,140 @@ watch(groupStore, async (newV, oldV) => {
const data = ref([])
const totalPage = ref(0)
$http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
data.value = res.data.list
totalPage.value = res.data.total_page
})
const updateList = function () {
$http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
data.value = res.data.list
totalPage.value = res.data.total_page
})
}
const updateGroupList = function () {
$http.post("/api/group/list").then(res => {
groupList.value = res.data
})
}
updateList()
updateGroupList()
const rowClick = function (row, column, event) {
router.push("/detail/" + row.id)
}
const markRead = function () {
let rows = taskTableDataRef.value?.getSelectionRows()
let ids = []
rows.forEach(element => {
ids.push(element.id)
});
$http.post("/api/email/read", { "ids": ids }).then(res => {
if (res.errorNo == 0) {
updateList()
} else {
ElMessage({
type: 'error',
message: res.errorMsg,
})
}
})
}
const move = function (group_id) {
let rows = taskTableDataRef.value?.getSelectionRows()
let ids = []
rows.forEach(element => {
ids.push(element.id)
});
ElMessageBox.confirm(
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,
})
}
})
})
}
const del = function () {
let rows = taskTableDataRef.value?.getSelectionRows()
let ids = []
rows.forEach(element => {
ids.push(element.id)
});
ElMessageBox.confirm(
lang.del_email_confirm,
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
)
.then(() => {
$http.post("/api/email/del", { "ids": ids }).then(res => {
if (res.errorNo == 0) {
updateList()
ElMessage({
type: 'success',
message: 'Delete completed',
})
} else {
ElMessage({
type: 'error',
message: res.errorMsg,
})
}
})
})
}
const rowStyle = function ({ row, rowIndwx }) {
return { 'cursor': 'pointer' }
}
const pageChange = function (p) {
$http.post("/api/email/list", { tag: tag, page_size: 10, current_page: p }).then(res => {
data.value = res.data.list
})
}
</script>
<style scoped>
#action {
text-align: left;
font-size: 20px;
line-height: 40px;
padding-left: 10px;
margin-right: 5px;
display: flex;
flex-direction: row;
}

View File

@ -1,7 +1,7 @@
<template>
<div id="main">
<div id="form">
<el-form :model="form" label-width="120px">
<el-form :model="form" label-width="120px" @keyup.enter.native="onSubmit">
<el-form-item :label="lang.account">
<el-input v-model="form.account" placeholder="User Name" />
</el-form-item>

View File

@ -26,7 +26,6 @@ export default defineConfig({
proxy: {
"/api": "http://127.0.0.1/",
"/attachments":"http://127.0.0.1/"
}
}
})

View File

@ -1,5 +1,5 @@
{
"logLevel": "debug",
"logLevel": "info",
"domain": "domain.com",
"webDomain": "mail.domain.com",
"dkimPrivateKeyPath": "config/dkim/dkim.priv",
@ -12,5 +12,6 @@
"weChatPushSecret": "",
"weChatPushTemplateId": "",
"weChatPushUserId": "",
"isInit": false
"isInit": true,
"httpsEnabled": 2
}

View File

@ -33,7 +33,7 @@ type Config struct {
//go:embed tables/*
var tableConfig embed.FS
const Version = "2.0.1"
const Version = "2.1.0"
const DBTypeMySQL = "mysql"
const DBTypeSQLite = "sqlite"

View File

@ -12,6 +12,6 @@
"weChatPushSecret": "",
"weChatPushTemplateId": "",
"weChatPushUserId": "",
"isInit": false,
"httpsEnabled": 0
"isInit": true,
"httpsEnabled": 2
}

View File

@ -0,0 +1,17 @@
{
"logLevel": "info",
"domain": "domain.com",
"webDomain": "mail.domain.com",
"dkimPrivateKeyPath": "config/dkim/dkim.priv",
"sslType": "0",
"SSLPrivateKeyPath": "config/ssl/private.key",
"SSLPublicKeyPath": "config/ssl/public.crt",
"dbDSN": "root:root@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local",
"dbType": "mysql",
"weChatPushAppId": "",
"weChatPushSecret": "",
"weChatPushTemplateId": "",
"weChatPushUserId": "",
"isInit": true,
"httpsEnabled": 2
}

View File

@ -0,0 +1 @@
这里存储的秘钥仅开发测试使用,线上环境请重新生成!

View File

@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANgOYlTcSbWqw54y
ISRYq6keu/fPj/m7CiVaXtOc9bJhI5uBsd8Jz1eIRzNmqLITL2e5lz2vpQAmpk3z
8Qd2iGCbZBwMXcfPVhAasGH367GRnJw3QPybA0Maob8CnfKDH6Pks3bX7xeef/U3
ufHYnwgPZUi121ECvPjZLEmXpBrvAgMBAAECgYAxdeGG4cMyBnyvy3QQ2Qe7OKD5
Uxf3qJzi/jQ1J3qLsncvU1p/38QKmtUJ7Fd0JLY2faMk6P/R8AckU1L7TWRcpafY
fUU4xpuTHpBnMhglOGjEoOfSUFh9iieG8cVreozOOfihFRgRUxu6zfxycymHjGxF
WbdK1zoNLgFgM0DWEQJBAOnkLf4P+pDMCeA/60pll39gIuW+AlMpWrjuA3Yhdu4B
BvC5ea3fIVCWiksfBKihXDZkLG9PZDipy2WiNypPx0kCQQDsep7qHiBPpcp4a9OU
KXolG771iHODId2Nc3zez2xG+2pY5BzoHD2TFdW1/5v9d4q+6u6dUb8v/t2GrMIQ
wrh3AkEAiO0Dm+wA1YoN8hGZjqlhArnmVDdjpwnbyc3Viu/Wb0l8par/uDGbkFFB
Tu8uzAYDNPh6JwQEeUO2Bp7rysJ/uQJBAJKq7rsX2kRr+Gq9vaksHHS9g693ZOVU
8LuVgEIU9fwEXQ4q1P7k3Q/HwBe0JESNiwEkZsAt/l0/PpgTt/17N7sCQBQKcd7e
++RuJCiMc0vMSYAKAmiARJHv/YoxS4tLngtrhu0h5uhr+35c0kJvy6nX0VBb5KV8
hUK3axAHTSO793o=
-----END PRIVATE KEY-----

View File

@ -0,0 +1 @@
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDmJU3Em1qsOeMiEkWKupHrv3z4/5uwolWl7TnPWyYSObgbHfCc9XiEczZqiyEy9nuZc9r6UAJqZN8/EHdohgm2QcDF3Hz1YQGrBh9+uxkZycN0D8mwNDGqG/Ap3ygx+j5LN21+8Xnn/1N7nx2J8ID2VItdtRArz42SxJl6Qa7wIDAQAB

View File

@ -0,0 +1 @@
这里存储的秘钥仅开发测试使用,线上环境请重新生成!

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDhh1fqAdYCSifPaCGfm2CjngcPaS7XehYdojBPPp5oEorWZFo1
3Rghfh7qIXYp4nruUwfKgIPM95nYNqTnXolhUm5ywYDAhfJquuquJ7cAPhyx4SwG
o58RgcJCM04yVKdxivskMiJvgUDz1ymdf6OA7MMbgGEcdky73TdC3FkfZQIDAQAB
AoGAe5shPPj6oVChVxSccQzIv4QqHHEqoiCgpGczEQuh6CpZe72Oj7z4r8qfCPWD
/NrLQ3mwaHVdR2ZhJFZ2tPRkWDI6hlK3IrC7A9LGoYCeV0AiDgMvzhQrWFjaw92u
V9OS7tgYkewimcDaK1eF1hH+GOh2aToWMKAgCCOoXjuVhsECQQD4lc/4/Km7hrEQ
JQGLOnB4qxdFk3HflShsXjnc/ydapQ5zCzCuCO7X89Jr8KpegC1SVmFR3YRjPNG4
R36yLR8RAkEA6EF45wYmQfAi9w0AMlmnXanHcWXBauOrbqv2M9cjpsBvxcIjRvHp
fca/LjEBf74X4YBhCCN1P7zRPLO2tYJjFQJAFdsGF/wO6D/lXWgDhLw0m0dfmmxm
PKQek7iNGdMNILkWViMLuqFqbm4vd/IG6JwYX/7cO5hgRWFZhvwyNXQmIQJBAOPX
Dqb79l3rGDHpVA8Qukn8+sV4gBS+wXcxRLY4UCYOU9fZikfXmymi5fuHYaQSNFUo
XofgWO4s6co1toA7J70CQQDym7XIGA0GTtUtBpvRU2ew3d6JItMFjzQCjgKJ/4zm
VqHIzyGoikq8jvbAqGJqRU72F/jfWEZlQO1KZemhmd/S
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICcjCCAdsCFDLeR0jPO+9Q/nadNrHGI3EG2eHoMA0GCSqGSIb3DQEBCwUAMHgx
CzAJBgNVBAYTAkNOMQswCQYDVQQIDAJCSjEQMA4GA1UEBwwHQmVpSmluZzENMAsG
A1UECgwETnVsbDENMAsGA1UECwwETnVsbDEOMAwGA1UEAwwFUE1haWwxHDAaBgkq
hkiG9w0BCQEWDWlAamlubnJyeS5jb20wHhcNMjMwODIzMTMyMTM0WhcNMzMwODIw
MTMyMTM0WjB4MQswCQYDVQQGEwJDTjELMAkGA1UECAwCQkoxEDAOBgNVBAcMB0Jl
aUppbmcxDTALBgNVBAoMBE51bGwxDTALBgNVBAsMBE51bGwxDjAMBgNVBAMMBVBN
YWlsMRwwGgYJKoZIhvcNAQkBFg1pQGppbm5ycnkuY29tMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDhh1fqAdYCSifPaCGfm2CjngcPaS7XehYdojBPPp5oEorW
ZFo13Rghfh7qIXYp4nruUwfKgIPM95nYNqTnXolhUm5ywYDAhfJquuquJ7cAPhyx
4SwGo58RgcJCM04yVKdxivskMiJvgUDz1ymdf6OA7MMbgGEcdky73TdC3FkfZQID
AQABMA0GCSqGSIb3DQEBCwUAA4GBAMR6M83L2V9YFcYLxUv3Vaf7KrSSvuiGl/6H
e2bMxboC8NBdsmRRhuKamti+NOe7i+BXTZ9TSy3zLQGK5LNvNOnWHHGj4vmVXoUV
rFBMY1Vf2ZiEtO0OQjEcLOpzXhVWyZuDt2HhMRj92ESeXUSCMPZWT2UfZZTm0fhv
ORq+I8O9
-----END CERTIFICATE-----

View File

@ -2,6 +2,7 @@ CREATE table email
(
id INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
type tinyint(4) NOT NULL DEFAULT 0 COMMENT '邮件类型0:收到的邮件1:发送的邮件',
group_id int unsigned NOT NULL DEFAULT 0 COMMENT '分组id',
subject varchar(1000) NOT NULL DEFAULT '' COMMENT '邮件标题',
reply_to json COMMENT '回复人',
from_name varchar(50) NOT NULL DEFAULT '' COMMENT '发件人名称',

View File

@ -0,0 +1,7 @@
CREATE TABLE `group`
(
id INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
name varchar(10) NOT NULL DEFAULT '' COMMENT '分组名称',
user_id INT unsigned NOT NULL DEFAULT 0 COMMENT '用户id',
parent_id INT unsigned COMMENT '父级组ID'
)COMMENT='分组信息表'

View File

@ -1,17 +1,18 @@
CREATE table email
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
type tinyint(4) NOT NULL DEFAULT 0 ,
subject varchar(1000) NOT NULL DEFAULT '' ,
reply_to json ,
from_name varchar(50) NOT NULL DEFAULT '' ,
from_address varchar(150) NOT NULL DEFAULT '' ,
`to` json ,
bcc json ,
cc json ,
`text` text ,
html text ,
sender json ,
type tinyint(4) NOT NULL DEFAULT 0,
group_id INTEGER NOT NULL DEFAULT 0,
subject varchar(1000) NOT NULL DEFAULT '',
reply_to json,
from_name varchar(50) NOT NULL DEFAULT '',
from_address varchar(150) NOT NULL DEFAULT '',
`to` json,
bcc json,
cc json,
`text` text,
html text,
sender json,
attachments json ,
spf_check tinyint(1) DEFAULT 0 ,
dkim_check tinyint(1) DEFAULT 0 ,

View File

@ -0,0 +1,7 @@
CREATE TABLE `group`
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name varchar(10) NOT NULL DEFAULT '',
parent_id INTEGER NOT NULL DEFAULT 0,
user_id INTEGER NOT NULL DEFAULT 0
)

View File

@ -0,0 +1,40 @@
package email
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"io"
"net/http"
"pmail/dto"
"pmail/dto/response"
"pmail/services/del_email"
)
type emailDeleteRequest struct {
IDs []int `json:"ids"`
}
func EmailDelete(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
reqBytes, err := io.ReadAll(req.Body)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
var reqData emailDeleteRequest
err = json.Unmarshal(reqBytes, &reqData)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
if len(reqData.IDs) <= 0 {
response.NewErrorResponse(response.ParamsError, "ID错误", "").FPrint(w)
return
}
err = del_email.DelEmail(ctx, reqData.IDs)
if err != nil {
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
return
}
response.NewSuccessResponse("success").FPrint(w)
}

View File

@ -0,0 +1,40 @@
package email
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"io"
"net/http"
"pmail/dto"
"pmail/dto/response"
"pmail/services/group"
)
type moveRequest struct {
GroupId int `json:"group_id"`
IDs []int `json:"ids"`
}
func Move(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
reqBytes, err := io.ReadAll(req.Body)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
var reqData moveRequest
err = json.Unmarshal(reqBytes, &reqData)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
if len(reqData.IDs) <= 0 {
response.NewErrorResponse(response.ParamsError, "ID错误", "").FPrint(w)
return
}
if !group.MoveMailToGroup(ctx, reqData.IDs, reqData.GroupId) {
response.NewErrorResponse(response.ServerError, "Error", "").FPrint(w)
return
}
response.NewSuccessResponse("success").FPrint(w)
}

View File

@ -0,0 +1,43 @@
package email
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"io"
"net/http"
"pmail/dto"
"pmail/dto/response"
"pmail/services/detail"
)
type markReadRequest struct {
IDs []int `json:"ids"`
}
func MarkRead(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
reqBytes, err := io.ReadAll(req.Body)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
var reqData markReadRequest
err = json.Unmarshal(reqBytes, &reqData)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
if len(reqData.IDs) <= 0 {
response.NewErrorResponse(response.ParamsError, "ID错误", "").FPrint(w)
return
}
for _, id := range reqData.IDs {
detail.GetEmailDetail(ctx, id, true)
}
if err != nil {
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
return
}
response.NewSuccessResponse("success").FPrint(w)
}

View File

@ -1,27 +1,32 @@
package controllers
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"io"
"net/http"
"pmail/db"
"pmail/dto"
"pmail/dto/response"
"pmail/i18n"
"pmail/services/group"
"pmail/utils/array"
)
type groupItem struct {
Label string `json:"label"`
Tag string `json:"tag"`
Children []*groupItem `json:"children"`
func GetUserGroupList(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
infos := group.GetGroupList(ctx)
response.NewSuccessResponse(infos).FPrint(w)
}
func GetUserGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
retData := []*groupItem{
retData := []*group.GroupItem{
{
Label: i18n.GetText(ctx.Lang, "all_email"),
Children: []*groupItem{
Children: []*group.GroupItem{
{
Label: i18n.GetText(ctx.Lang, "inbox"),
Tag: dto.SearchTag{Type: 0, Status: -1}.ToString(),
Tag: dto.SearchTag{Type: 0, Status: -1, GroupId: 0}.ToString(),
},
{
Label: i18n.GetText(ctx.Lang, "outbox"),
@ -35,5 +40,59 @@ func GetUserGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
},
}
retData = array.Merge(retData, group.GetGroupInfoList(ctx))
response.NewSuccessResponse(retData).FPrint(w)
}
type addGroupRequest struct {
Name string `json:"name"`
ParentId int `json:"parent_id"`
}
func AddGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
var reqData *addGroupRequest
reqBytes, err := io.ReadAll(req.Body)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
err = json.Unmarshal(reqBytes, &reqData)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
res, err := db.Instance.Exec(db.WithContext(ctx, "insert into `group` (name,parent_id,user_id) values (?,?,?)"), reqData.Name, reqData.ParentId, ctx.UserInfo.ID)
if err != nil {
response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w)
return
}
id, err := res.LastInsertId()
if err != nil {
response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w)
return
}
response.NewSuccessResponse(id).FPrint(w)
}
type delGroupRequest struct {
Id int `json:"id"`
}
func DelGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
var reqData *delGroupRequest
reqBytes, err := io.ReadAll(req.Body)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
err = json.Unmarshal(reqBytes, &reqData)
if err != nil {
log.WithContext(ctx).Errorf("%+v", err)
}
succ, err := group.DelGroup(ctx, reqData.Id)
if err != nil {
response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w)
return
}
response.NewSuccessResponse(succ).FPrint(w)
}

View File

@ -9,6 +9,7 @@ import (
"pmail/config"
"pmail/dto"
"pmail/utils/errors"
"strings"
)
var Instance *sqlx.DB
@ -32,6 +33,8 @@ func Init() error {
Instance.SetMaxIdleConns(10)
//showMySQLCharacterSet()
checkTable()
// 处理版本升级带来的数据表变更
databaseUpdate()
return nil
}
@ -84,21 +87,27 @@ func checkTable() {
}
}
func showMySQLCharacterSet() {
var res []struct {
Variable_name string `db:"Variable_name"`
Value string `db:"Value"`
}
err := Instance.Select(&res, "show variables like '%character%';")
log.Debugf("%+v %+v", res, err)
type tableSQL struct {
Table string `db:"Table"`
CreateTable string `db:"Create Table"`
}
func testSlowLog() {
var res []struct {
Value string `db:"Value"`
func databaseUpdate() {
// 检查email表是否有group id
var err error
var res []tableSQL
if config.Instance.DbType == "sqlite" {
err = Instance.Select(&res, "select sql as `Create Table` from sqlite_master where type='table' and tbl_name = 'email'")
} else {
err = Instance.Select(&res, "show create table `email`")
}
if err != nil {
panic(err)
}
if len(res) > 0 && !strings.Contains(res[0].CreateTable, "group_id") {
Instance.Exec("alter table email add group_id integer default 0 not null;")
}
err := Instance.Select(&res, "/* asddddasad */select /* this is test */ sleep(4) as Value")
log.Debugf("%+v %+v", res, err)
}

View File

@ -3,8 +3,9 @@ package dto
import "encoding/json"
type SearchTag struct {
Type int `json:"type"`
Status int `json:"status"`
Type int `json:"type"` // -1 不限
Status int `json:"status"` // -1 不限
GroupId int `json:"group_id"` // -1 不限
}
func (t SearchTag) ToString() string {

View File

@ -42,8 +42,14 @@ func HttpStart() {
mux.HandleFunc("/api/ping", contextIterceptor(controllers.Ping))
mux.HandleFunc("/api/login", contextIterceptor(controllers.Login))
mux.HandleFunc("/api/group", contextIterceptor(controllers.GetUserGroup))
mux.HandleFunc("/api/group/list", contextIterceptor(controllers.GetUserGroupList))
mux.HandleFunc("/api/group/add", contextIterceptor(controllers.AddGroup))
mux.HandleFunc("/api/group/del", contextIterceptor(controllers.DelGroup))
mux.HandleFunc("/api/email/list", contextIterceptor(email.EmailList))
mux.HandleFunc("/api/email/del", contextIterceptor(email.EmailDelete))
mux.HandleFunc("/api/email/read", contextIterceptor(email.MarkRead))
mux.HandleFunc("/api/email/detail", contextIterceptor(email.EmailDetail))
mux.HandleFunc("/api/email/move", contextIterceptor(email.Move))
mux.HandleFunc("/api/email/send", contextIterceptor(email.Send))
mux.HandleFunc("/api/settings/modify_password", contextIterceptor(controllers.ModifyPassword))
mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))

View File

@ -52,9 +52,15 @@ func HttpsStart() {
mux.HandleFunc("/api/ping", contextIterceptor(controllers.Ping))
mux.HandleFunc("/api/login", contextIterceptor(controllers.Login))
mux.HandleFunc("/api/group", contextIterceptor(controllers.GetUserGroup))
mux.HandleFunc("/api/group/list", contextIterceptor(controllers.GetUserGroupList))
mux.HandleFunc("/api/group/add", contextIterceptor(controllers.AddGroup))
mux.HandleFunc("/api/group/del", contextIterceptor(controllers.DelGroup))
mux.HandleFunc("/api/email/list", contextIterceptor(email.EmailList))
mux.HandleFunc("/api/email/read", contextIterceptor(email.MarkRead))
mux.HandleFunc("/api/email/del", contextIterceptor(email.EmailDelete))
mux.HandleFunc("/api/email/detail", contextIterceptor(email.EmailDetail))
mux.HandleFunc("/api/email/send", contextIterceptor(email.Send))
mux.HandleFunc("/api/email/move", contextIterceptor(email.Move))
mux.HandleFunc("/api/settings/modify_password", contextIterceptor(controllers.ModifyPassword))
mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))

View File

@ -6,7 +6,6 @@ import (
log "github.com/sirupsen/logrus"
"os"
"pmail/config"
"pmail/cron_server"
"pmail/dto"
"pmail/res_init"
"time"
@ -80,7 +79,7 @@ func main() {
log.Infoln("***************************************************")
// 定时任务启动
go cron_server.Start()
//go cron_server.Start()
// 核心服务启动
res_init.Init()

View File

@ -9,6 +9,7 @@ import (
type Email struct {
Id int `db:"id" json:"id"`
Type int8 `db:"type" json:"type"`
GroupId int `db:"group_id" json:"group_id"`
Subject string `db:"subject" json:"subject"`
ReplyTo string `db:"reply_to" json:"reply_to"`
FromName string `db:"from_name" json:"from_name"`

8
server/models/group.go Normal file
View File

@ -0,0 +1,8 @@
package models
type Group struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
ParentId int `db:"parent_id" json:"parent_id"`
UserId int `db:"user_id" json:"-"`
}

View File

@ -0,0 +1,32 @@
package del_email
import (
"fmt"
"pmail/db"
"pmail/dto"
"pmail/models"
"pmail/services/auth"
"pmail/utils/array"
"pmail/utils/errors"
)
func DelEmail(ctx *dto.Context, ids []int) error {
var emails []*models.Email
db.Instance.Select(&emails, db.WithContext(ctx, fmt.Sprintf("select * from email where id in (%s)", array.Join(ids, ","))))
for _, email := range emails {
// 检查是否有权限
hasAuth := auth.HasAuth(ctx, email)
if !hasAuth {
return errors.New("No Auth!")
}
}
_, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("delete from email where id in (%s)", array.Join(ids, ","))))
if err != nil {
return errors.Wrap(err)
}
return nil
}

View File

@ -0,0 +1,114 @@
package group
import (
"fmt"
log "github.com/sirupsen/logrus"
"pmail/db"
"pmail/dto"
"pmail/models"
"pmail/utils/array"
"pmail/utils/errors"
)
type GroupItem struct {
Id int `json:"id"`
Label string `json:"label"`
Tag string `json:"tag"`
Children []*GroupItem `json:"children"`
}
func DelGroup(ctx *dto.Context, groupId int) (bool, error) {
allGroupIds := getAllChildId(ctx, groupId)
allGroupIds = append(allGroupIds, groupId)
// 开启一个事务
trans, err := db.Instance.Begin()
if err != nil {
return false, errors.Wrap(err)
}
res, err := trans.Exec(db.WithContext(ctx, fmt.Sprintf("delete from `group` where id in (%s) and user_id =?", array.Join(allGroupIds, ","))), ctx.UserInfo.ID)
if err != nil {
trans.Rollback()
return false, errors.Wrap(err)
}
num, err := res.RowsAffected()
if err != nil {
trans.Rollback()
return false, errors.Wrap(err)
}
_, err = trans.Exec(db.WithContext(ctx, fmt.Sprintf("update email set group_id=0 where group_id in (%s)", array.Join(allGroupIds, ","))))
if err != nil {
trans.Rollback()
return false, errors.Wrap(err)
}
trans.Commit()
return num > 0, nil
}
type id struct {
Id int `db:"id"`
}
func getAllChildId(ctx *dto.Context, rootId int) []int {
var ids []id
var ret []int
db.Instance.Select(&ids, db.WithContext(ctx, "select id from `group` where parent_id=? and user_id=?"), rootId, ctx.UserInfo.ID)
for _, item := range ids {
ret = array.Merge(ret, getAllChildId(ctx, item.Id))
ret = append(ret, item.Id)
}
return ret
}
// GetGroupInfoList 获取全部的分组
func GetGroupInfoList(ctx *dto.Context) []*GroupItem {
return buildChildren(ctx, 0)
}
// MoveMailToGroup 将某封邮件移动到某个分组中
func MoveMailToGroup(ctx *dto.Context, mailId []int, groupId int) bool {
res, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("update email set group_id=? where id in (%s)", array.Join(mailId, ","))), groupId)
if err != nil {
log.WithContext(ctx).Errorf("SQL Error:%+v", err)
return false
}
rowNum, err := res.RowsAffected()
if err != nil {
log.WithContext(ctx).Errorf("SQL Error:%+v", err)
return false
}
return rowNum > 0
}
func buildChildren(ctx *dto.Context, parentId int) []*GroupItem {
var ret []*GroupItem
var rootGroup []*models.Group
err := db.Instance.Select(&rootGroup, db.WithContext(ctx, "select * from `group` where parent_id=? and user_id=?"), parentId, ctx.UserInfo.ID)
if err != nil {
log.WithContext(ctx).Errorf("SQL Error:%v", err)
}
for _, group := range rootGroup {
ret = append(ret, &GroupItem{
Id: group.ID,
Label: group.Name,
Tag: dto.SearchTag{GroupId: group.ID, Status: -1, Type: -1}.ToString(),
Children: buildChildren(ctx, group.ID),
})
}
return ret
}
func GetGroupList(ctx *dto.Context) []*models.Group {
var ret []*models.Group
db.Instance.Select(&ret, db.WithContext(ctx, "select * from `group` where user_id=?"), ctx.UserInfo.ID)
return ret
}

View File

@ -48,6 +48,11 @@ func genSQL(ctx *dto.Context, counter bool, tag, keyword string, offset, limit i
sqlParams = append(sqlParams, tagInfo.Status)
}
if tagInfo.GroupId != -1 {
sql += " and group_id=? "
sqlParams = append(sqlParams, tagInfo.GroupId)
}
if keyword != "" {
sql += " and (subject like ? or text like ? )"
sqlParams = append(sqlParams, "%"+keyword+"%", "%"+keyword+"%")

View File

@ -0,0 +1,10 @@
package setup
import (
"testing"
)
func TestSetAdminPassword(t *testing.T) {
SetAdminPassword(nil, "admin", "admin")
}

View File

@ -0,0 +1,10 @@
package password
import (
"fmt"
"testing"
)
func TestEncode(t *testing.T) {
fmt.Println(Encode("admin"))
}