jinnrry 2 anni fa
parent
commit
5ef5057032

+ 97 - 0
fe/src/components/GroupSettings.vue

@@ -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>

+ 6 - 0
fe/src/components/HomeAside.vue

@@ -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>

+ 6 - 1
fe/src/components/HomeHeader.vue

@@ -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)

+ 14 - 0
fe/src/i18n/i18n.js

@@ -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) {

+ 142 - 16
fe/src/views/ListView.vue

@@ -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;
 }
 
 

+ 1 - 1
fe/src/views/LoginView.vue

@@ -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>

+ 0 - 1
fe/vite.config.js

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

+ 3 - 2
server/config/config.dev.json

@@ -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
 }

+ 1 - 1
server/config/config.go

@@ -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"

+ 2 - 2
server/config/config.json

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

+ 17 - 0
server/config/config_mysql.json

@@ -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
+}

+ 1 - 0
server/config/dkim/README.md

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

+ 16 - 0
server/config/dkim/dkim.priv

@@ -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-----

+ 1 - 0
server/config/dkim/dkim.public

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

+ 1 - 0
server/config/ssl/README.md

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

+ 15 - 0
server/config/ssl/private.key

@@ -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-----

+ 16 - 0
server/config/ssl/public.crt

@@ -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-----

+ 1 - 0
server/config/tables/mysql/email.sql

@@ -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 '发件人名称',

+ 7 - 0
server/config/tables/mysql/group.sql

@@ -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='分组信息表'

+ 12 - 11
server/config/tables/sqlite/email.sql

@@ -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 ,

+ 7 - 0
server/config/tables/sqlite/group.sql

@@ -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
+)

+ 40 - 0
server/controllers/email/delete.go

@@ -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)
+
+}

+ 40 - 0
server/controllers/email/move.go

@@ -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)
+
+}

+ 43 - 0
server/controllers/email/read.go

@@ -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)
+
+}

+ 66 - 7
server/controllers/group.go

@@ -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)
+}

+ 21 - 12
server/db/init.go

@@ -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"`
+type tableSQL struct {
+	Table       string `db:"Table"`
+	CreateTable string `db:"Create Table"`
+}
+
+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`")
 	}
-	err := Instance.Select(&res, "show variables like '%character%';")
-	log.Debugf("%+v  %+v", res, err)
 
-}
+	if err != nil {
+		panic(err)
+	}
 
-func testSlowLog() {
-	var res []struct {
-		Value string `db:"Value"`
+	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)
 
 }

+ 3 - 2
server/dto/tag.go

@@ -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 {

+ 6 - 0
server/http_server/http_server.go

@@ -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))

+ 6 - 0
server/http_server/https_server.go

@@ -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))

+ 1 - 2
server/main.go

@@ -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()

+ 1 - 0
server/models/email.go

@@ -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 - 0
server/models/group.go

@@ -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:"-"`
+}

+ 32 - 0
server/services/del_email/del_email.go

@@ -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
+}

+ 114 - 0
server/services/group/group.go

@@ -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
+}

+ 5 - 0
server/services/list/list.go

@@ -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+"%")

+ 10 - 0
server/services/setup/db_test.go

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

+ 10 - 0
server/utils/password/encode_test.go

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