jinnrry há 2 anos atrás
pai
commit
318fea24ad
41 ficheiros alterados com 888 adições e 373 exclusões
  1. 29 51
      README.md
  2. 20 66
      README_CN.md
  3. BIN
      build.sh
  4. BIN
      docs/en.gif
  5. 1 1
      fe/src/http/http.js
  6. 13 2
      fe/src/i18n/i18n.js
  7. 1 1
      fe/src/views/EditerView.vue
  8. 148 8
      fe/src/views/SetupView.vue
  9. 6 4
      server/config/config.dev.json
  10. 5 1
      server/config/config.go
  11. 6 4
      server/config/config.json
  12. 0 1
      server/config/dkim/README
  13. 0 2
      server/config/tables/mysql/data/user.sql
  14. 0 2
      server/config/tables/mysql/data/user_auth.sql
  15. 0 2
      server/config/tables/sqlite/data/user.sql
  16. 0 2
      server/config/tables/sqlite/data/user_auth.sql
  17. 11 0
      server/controllers/interceptor.go
  18. 2 9
      server/controllers/login.go
  19. 2 1
      server/controllers/settings.go
  20. 65 12
      server/controllers/setup.go
  21. 34 0
      server/cron_server/ssl_update.go
  22. 5 3
      server/db/init.go
  23. 11 12
      server/hooks/wechat_push/wechat_push.go
  24. 35 0
      server/http_server/http_server.go
  25. 19 51
      server/http_server/https_server.go
  26. 63 0
      server/http_server/setup_server.go
  27. 23 3
      server/main.go
  28. 25 7
      server/res_init/init.go
  29. 1 1
      server/services/auth/auth.go
  30. 7 0
      server/services/auth/auth_test.go
  31. 53 9
      server/services/setup/db.go
  32. 1 1
      server/services/setup/dns.go
  33. 11 3
      server/services/setup/domain.go
  34. 24 0
      server/services/setup/finish.go
  35. 0 103
      server/services/setup/ssl.go
  36. 37 0
      server/services/setup/ssl/challenge.go
  37. 172 0
      server/services/setup/ssl/ssl.go
  38. 17 0
      server/services/setup/ssl/ssl_test.go
  39. 4 0
      server/signal/signal.go
  40. 19 11
      server/smtp_server/main.go
  41. 18 0
      server/utils/password/encode.go

+ 29 - 51
README.md

@@ -1,12 +1,21 @@
-# PMail 
+# PMail
 
-> The current code is not stable, be sure to record the log! Lost letters or letters parsed wrong can find out the original content of the mail from the log!
+> The current code is not stable, be sure to record the log! Lost letters or letters parsed wrong can find out the
+> original content of the mail from the log!
 
 ## [中文文档](./README_CN.md)
 
 ## Introduction
 
-An extremely lightweight mailbox server designed for personal use scenarios. 
+PMail is a personal email server that pursues a minimal deployment process and extreme resource consumption. It runs on
+a single file and contains complete send/receive mail service and web-side mail management functions. Just a server , a
+domain name , a line of code , a minute of deployment time , you will be able to build a domain name mailbox of your
+own .
+
+Any project related Issue, PR is welcome.At present, the project UI design is ugly, UI interaction experience is poor,
+welcome all UI, designers, front-end guidance. Finally, also for this project to solicit a beautiful and lovely Logo!
+
+<img src="./docs/en.gif" alt="Editor" width="800px">
 
 ## Features
 
@@ -16,36 +25,29 @@ An extremely lightweight mailbox server designed for personal use scenarios.
 
 * Support dkim, spf checksum, [Email Test](https://www.mail-tester.com/) score 10 points if correctly configured.
 
+* Implementing the ACME protocol, the program will automatically obtain and update Let's Encrypt certificates.
+
 ## Disadvantages
 
-* At present, only the core function of sending and receiving emails has been completed. Basically, it can only be used by a single person, and does not deal with issues related to permission management in the process of multiple users.
+* At present, only the core function of sending and receiving emails has been completed. Basically, it can only be used
+  by a single person, and does not deal with issues related to permission management in the process of multiple users.
 
 * The UI is ugly
 
 # How to run
 
-## 1、Generate DKIM secret key
-
-Generate public and private keys by the dkim-keygen tool of the [go-msgauth](https://github.com/emersion/go-msgauth) project
+## 1、Download
 
-Put the key in the `config/dkim` directory.
+[Click Here](https://github.com/Jinnrry/PMail/releases) Download a program file that matches you.
 
-## 2、Set DNS
+## 2、Run
 
-Add the following records to your domain DNS settings
+`double-click to open` Or `execute command to run`
 
-| type | hostname             | address / value      |
-|------|----------------------|----------------------|
-| A    | smtp                 | server ip            |
-| MX   | _                    | smtp.YourDomain      |
-| TXT  | _                    | v=spf1 a mx ~all     |
-| TXT  | default._domainkey	  | Your DKIM public key |
+## 3、Configuration
 
-## 3、Domain SSL Key
-
-Prepare the certificate of `smtp.YourDomain`, the private key in ".key" format and the public key in ".crt" format
-
-Put the certificate in the `config/ssl` directory.
+Open `http://127.0.0.1` in your browser or use your server's public IP to visit, then follow the instructions to
+configure.
 
 ## 4、Build(or download)
 
@@ -59,35 +61,15 @@ Put the certificate in the `config/ssl` directory.
 
 Modify the `config.json` file in the config directory and fill in your secret key and domain information.
 
-Tips:
-
-MySQL database name must is `pmail`, and charset must is `utf8_general_ci`.
+## 6、Email Test
 
-Configuration file description :
-```json
-{
-  "domain": "demo.com", // Your domain
-  "dkimPrivateKeyPath": "config/dkim/dkim.priv",  // dkim private key
-  "SSLPrivateKeyPath": "config/ssl/private.key",  // ssl private key
-  "SSLPublicKeyPath": "config/ssl/public.crt",    // ssl public key
-  "mysqlDSN": "username:password@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local", // mysql connect infonation
-  "weChatPushAppId": "",  // WeChat public account appid (for new email message push) . If you don't need it, you can make it empty.
-  "weChatPushSecret": "",   // WeChat api secret
-  "weChatPushTemplateId": "",  // push template id
-  "weChatPushUserId": "" // wechat user id
-}
-```
+Check if your mailbox has completed all the security configuration. It is recommended to
+use [https://www.mail-tester.com/](https://www.mail-tester.com/) for checking.
 
-## 6、Run
-
-exec `pmail` and check port of 25、80.
-
-The webmail service address is http://yourip. Default account is `admin` and password is `admin`
-
-## 7、Email Test
-
-Check if your mailbox has completed all the security configuration. It is recommended to use [https://www.mail-tester.com/](https://www.mail-tester.com/) for checking. 
+## 7、 WeChat Message Push
 
+Open the `config/config.json` file in the run directory, edit a few configuration items at the beginning of `weChatPush`
+and restart the service.
 
 # For Developer
 
@@ -104,7 +86,3 @@ The code is in `server` folder.
 ## Plugin Development
 
 Reference this file. `server/hooks/wechat_push/wechat_push.go`
-
-# What's More
-
-Welcome PR! Welcome Issues! The project need a Logo !

+ 20 - 66
README_CN.md

@@ -1,7 +1,13 @@
-# PMail 
+# PMail
 
 > Welcome PR! Welcome Issues! 目前代码并不稳定,一定记录好日志!丢信或者信件解析错误可以从日志中找出邮件原始内容!
 
+PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱服务器。单文件运行,包含完整的收发邮件服务和Web端邮件管理功能。只需一台服务器、一个域名、一行代码、一分钟部署时间,你就能够搭建出一个自己的域名邮箱。
+
+目前项目UI设计比较丑陋、UI交互体验较差,欢迎各位UI、设计师、前端提出指导意见。最后,也为这个项目征集一个漂亮可爱的Logo!
+
+<img src="./docs/cn.gif" alt="Editor" width="800px">
+
 ## 为什么写这个项目
 
 迫于越来越多的邮件服务商暂停了针对个人的域名邮箱服务(比如QQ邮箱、微软Outlook邮箱),因此考虑自建域名邮箱服务。
@@ -22,6 +28,10 @@
 
 支持dkim、spf校验。正确配置的情况下,Email Test得分10分。
 
+### 4、自动SSL证书
+
+实现了ACME协议,程序将自动获取并更新Let’s Encrypt证书。
+
 ## 其他
 
 ### 不足
@@ -35,78 +45,25 @@
 
 # 如何部署
 
-## 1、生成DKIM 秘钥
-
-```
-go install github.com/emersion/go-msgauth/cmd/dkim-keygen@latest
-dkim-keygen
-```
-执行后将得到`dkim.priv`文件,公钥数据会直接输出
-
-生成以后将密钥放到`config/dkim`目录中
-
-## 2、设置域名DNS
-
-添加以下记录到你到域名解析中
-
-| 类型  | 主机记录                | 记录值              |
-|-----|---------------------|------------------|
-| A   | smtp                | 服务器IP            |
-| MX  | _                   | smtp.你的域名        |
-| TXT | _                   | v=spf1 a mx ~all |
-| TXT | default._domainkey	 | 你生成的DKIM公钥       |
-
-## 3、申请域名证书
+## 1、下载文件
 
-准备好 `smtp.你的域名` 的证书,key格式的私钥和crt格式的公钥
+[点击这里](https://github.com/Jinnrry/PMail/releases)下载一个与你匹配的程序文件。
 
-放到`config/ssl`目录中
+## 2、运行
 
-## 4、编译程序(或者直接下载编译好的二进制文件)
+双击打开 OR 执行命令运行
 
-1、前端环境:安装好node环境,配置好yarn
+## 3、配置
 
-2、后端环境:安装最新的golang
+浏览器打开 `http://127.0.0.1` 或者是用你服务器公网IP访问,然后按提示配置
 
-3、执行`./build.sh`
-
-## 5、修改配置文件
-
-修改config目录中的`config.json`文件,填入你的秘钥与域名信息
-
-Tips:
-
-MySQL库名必须叫pmail,另外,数据库必须使用utf8_general_ci字符集
-
-配置文件说明:
-```json
-{
-  "domain": "demo.com", // 你的域名
-  "dkimPrivateKeyPath": "config/dkim/dkim.priv",  // dkim私钥
-  "SSLPrivateKeyPath": "config/ssl/private.key",  // ssl证书私钥
-  "SSLPublicKeyPath": "config/ssl/public.crt",    // ssl证书公钥
-  "mysqlDSN": "username:password@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local", // mysql连接信息
-  "weChatPushAppId": "",  //微信公众号id(用于新消息提醒),没有留空即可
-  "weChatPushSecret": "",   // 微信公众号api秘钥
-  "weChatPushTemplateId": "",  // 微信公众号推送模板id
-  "weChatPushUserId": "" // 微信推送用户id
-}
-```
-
-## 6、启动
-
-运行`PMail`程序,检查服务器25、80端口正常即可
-
-邮箱后台, http://yourip,默认账号admin,默认密码admin
-
-## 7、邮箱得分测试
+## 4、邮箱得分测试
 
 建议找一下邮箱测试服务(比如[https://www.mail-tester.com/](https://www.mail-tester.com/))进行邮件得分检测,避免自己某些步骤漏配,导致发件进对方垃圾箱。
 
-## 8、其他说明
-
-邮件是否进对方垃圾箱与程序无关、与你的服务器IP、服务器域名有关。我自己搭建的服务,测试了收发QQ、Gmail、Outlook、163、126均正常,无任何拦截,且不会进垃圾箱。
+## 5、微信推送
 
+打开运行目录下的 `config/config.json`文件,编辑 `weChatPush` 开头的几个配置项,重启服务即可。
 
 # 参与开发
 
@@ -124,6 +81,3 @@ MySQL库名必须叫pmail,另外,数据库必须使用utf8_general_ci字符
 
 参考微信推送插件`server/hooks/wechat_push/wechat_push.go`
 
-# 最后
-
-欢迎PR! 欢迎Issue!求个Logo!

BIN
build.sh


BIN
docs/en.gif


+ 1 - 1
fe/src/http/http.js

@@ -6,7 +6,7 @@ import lang from '../i18n/i18n';
 //创建axios的一个实例 
 var $http = axios.create({
     baseURL: import.meta.env.VITE_APP_URL, //接口统一域名
-    timeout: 6000, //设置超时
+    timeout: 60000, //设置超时
     headers: {
         'Content-Type': 'application/json;charset=UTF-8;',
         'Lang': lang.lang

+ 13 - 2
fe/src/i18n/i18n.js

@@ -37,6 +37,8 @@ var lang = {
     "setDNS": "Set DNS",
     "setSSL": "Set SSL",
     "setDatabase": "Set Database",
+    "setAdminPassword": "Set Password",
+    "admin_account": "Administrator Account",
     "setOther": "Other",
     "welcome": "Welcome",
     "next": "Next",
@@ -52,7 +54,10 @@ var lang = {
     "smtp_domain": "SMTP Domain",
     "web_domain": "Web Domain",
     "dns_desc": "Please add the following information to your DNS records",
-
+    "ssl_auto": "Automatically configure SSL certificates (recommended)",
+    "ssl_manuallyf": "Manually configure an SSL certificate",
+    "ssl_key_path": "ssl key file path",
+    "ssl_crt_path": "ssl crt file path",
 };
 
 
@@ -96,6 +101,8 @@ var zhCN = {
     "setDNS": "DNS设置",
     "setSSL": "SSL设置",
     "setDatabase": "数据库设置",
+    "setAdminPassword": "密码设置",
+    "admin_account": "管理员账号",
     "setOther": "其他设置",
     "welcome": "欢迎",
     "next": "下一步",
@@ -110,7 +117,11 @@ var zhCN = {
     "domain_desc": "设置你的域名信息。",
     "smtp_domain": "SMTP域名地址",
     "web_domain": "Web域名地址",
-    "dns_desc": "请将以下信息添加到DNS记录中"
+    "dns_desc": "请将以下信息添加到DNS记录中",
+    "ssl_auto": "自动配置SSL证书(推荐)",
+    "ssl_manuallyf": "手动配置SSL证书",
+    "ssl_key_path": "ssl key文件位置",
+    "ssl_crt_path": "ssl crt文件位置",
 }
 
 switch (navigator.language) {

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

@@ -135,7 +135,7 @@ const validateSender = function (rule, value, callback) {
 }
 
 const checkEmail = function (str) {
-    var re = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
+    var re = /.+@.+\..+/
     if (re.test(str)) {
         return true
     } else {

+ 148 - 8
fe/src/views/SetupView.vue

@@ -3,10 +3,10 @@
         <el-steps :active="active" align-center finish-status="success" id="status">
             <el-step :title="lang.welcome" />
             <el-step :title="lang.setDatabase" />
+            <el-step :title="lang.setAdminPassword" />
             <el-step :title="lang.SetDomail" />
             <el-step :title="lang.setDNS" />
             <el-step :title="lang.setSSL" />
-            <el-step :title="lang.setOther" />
         </el-steps>
 
 
@@ -48,6 +48,33 @@
 
 
         <div v-if="active == 2" class="ctn">
+            <div class="desc">
+                <h2>{{ lang.setAdminPassword }}</h2>
+                <!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
+            </div>
+            <div class="form" style="width: 400px;">
+                <el-form label-width="120px">
+
+                    <el-form-item :label="lang.admin_account">
+                        <el-input v-bind:disabled="adminSettings.hadSeted" placeholder="admin"
+                            v-model="adminSettings.account"></el-input>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.password">
+                        <el-input type="password" v-bind:disabled="adminSettings.hadSeted" placeholder=""
+                            v-model="adminSettings.password"></el-input>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.enter_again">
+                        <el-input type="password" v-bind:disabled="adminSettings.hadSeted" placeholder=""
+                            v-model="adminSettings.password2"></el-input>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+
+        <div v-if="active == 3" class="ctn">
             <div class="desc">
                 <h2>{{ lang.SetDomail }}</h2>
                 <!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
@@ -69,7 +96,7 @@
         </div>
 
 
-        <div v-if="active == 3" class="ctn_s">
+        <div v-if="active == 4" class="ctn_s">
             <div class="desc">
                 <h2>{{ lang.setDNS }}</h2>
                 <div style="margin-top: 10px;">{{ lang.dns_desc }}</div>
@@ -94,8 +121,34 @@
             </div>
         </div>
 
+        <div v-if="active == 5" class="ctn">
+            <div class="desc">
+                <h2>{{ lang.setSSL }}</h2>
+                <div style="margin-top: 10px;">{{ lang.setSSL }}</div>
+            </div>
+            <div class="form" width="600px">
+                <el-form label-width="120px">
+                    <el-form-item :label="lang.type">
+                        <el-select :placeholder="lang.ssl_auto" v-model="sslSettings.type">
+                            <el-option :label="lang.ssl_auto" value="0" />
+                            <el-option :label="lang.ssl_manuallyf" value="1" />
+                        </el-select>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.ssl_key_path" v-if="sslSettings.type == '1'">
+                        <el-input placeholder="./config/ssl/private.key" v-model="sslSettings.key_path"></el-input>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.ssl_crt_path" v-if="sslSettings.type == '1'">
+                        <el-input placeholder="./config/ssl/public.crt" v-model="sslSettings.crt_path"></el-input>
+                    </el-form-item>
+
+                </el-form>
+            </div>
+        </div>
 
-        <el-button id="next" style="margin-top: 12px" @click="next">{{ lang.next }}</el-button>
+        <el-button v-loading.fullscreen.lock="fullscreenLoading" id="next" style="margin-top: 12px" @click="next">{{
+            lang.next }}</el-button>
 
     </div>
 </template>
@@ -108,10 +161,16 @@ import { ElMessage } from 'element-plus'
 import router from "@/router";  //根路由对象
 import lang from '../i18n/i18n';
 
+const adminSettings = reactive({
+    "account": "admin",
+    "password": "",
+    "password2": "",
+    "hadSeted": false
+})
 
 const dbSettings = reactive({
-    "type": "",
-    "dsn": "",
+    "type": "sqlite",
+    "dsn": "./pmail.db",
     "lable": ""
 })
 
@@ -120,12 +179,57 @@ const domainSettings = reactive({
     "smtp_domain": ""
 })
 
+const sslSettings = reactive({
+    "type": "0",
+    "key_path": "./config/ssl/private.key",
+    "crt_path": "./config/ssl/public.crt"
+})
+
 const active = ref(0)
+const fullscreenLoading = ref(false)
+
 
 const dnsInfos = ref([
-    { "host": "smtp", "type": "A", "value": "YouServerIp", "prid": "NA", "ttl": "3600" }
 ])
 
+const setPassword = () => {
+    if (adminSettings.hadSeted) {
+        active.value++;
+        getDomainConfig();
+        return;
+    }
+
+    if (adminSettings.password != adminSettings.password2) {
+        ElMessage.error(lang.err_pwd_diff)
+    } else {
+        $http.post("/api/setup", { "action": "set", "step": "password", "account": adminSettings.account, "password": adminSettings.password }).then((res) => {
+            if (res.errorNo != 0) {
+                ElMessage.error(res.errorMsg)
+            } else {
+                active.value++;
+                getDomainConfig();
+            }
+        })
+    }
+}
+
+const getPassword = () => {
+    $http.post("/api/setup", { "action": "get", "step": "password" }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            adminSettings.hadSeted = res.data != ""
+            if (adminSettings.hadSeted) {
+                adminSettings.account = res.data
+                adminSettings.password = "*******"
+                adminSettings.password2 = "*******"
+            }
+
+        }
+    })
+}
+
+
 const getDbConfig = () => {
     $http.post("/api/setup", { "action": "get", "step": "database" }).then((res) => {
         if (res.errorNo != 0) {
@@ -154,7 +258,7 @@ const setDbConfig = () => {
             ElMessage.error(res.errorMsg)
         } else {
             active.value++;
-            getDomainConfig();
+            getPassword();
         }
     })
 }
@@ -169,6 +273,34 @@ const getDNSConfig = () => {
     })
 }
 
+
+const getSSLConfig = () => {
+    $http.post("/api/setup", { "action": "get", "step": "ssl" }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            sslSettings.type = res.data
+        }
+    })
+}
+
+
+const setSSLConfig = () => {
+    fullscreenLoading.value = true;
+    $http.post("/api/setup", { "action": "set", "step": "ssl", "ssl_type": sslSettings.type, "key_path": sslSettings.key_path, "crt_path": sslSettings.crt_path }).then((res) => {
+        if (res.errorNo != 0) {
+            fullscreenLoading.value = false;
+            ElMessage.error(res.errorMsg)
+        } else {
+            setTimeout(function () {
+                window.location.href = "https://" + domainSettings.web_domain;
+            }, 10000);
+
+        }
+    })
+}
+
+
 const setDomainConfig = () => {
     $http.post("/api/setup", { "action": "set", "step": "domain", "web_domain": domainSettings.web_domain, "smtp_domain": domainSettings.smtp_domain }).then((res) => {
         if (res.errorNo != 0) {
@@ -191,9 +323,17 @@ const next = () => {
             setDbConfig();
             break;
         case 2:
-            setDomainConfig();
+            setPassword();
             break;
         case 3:
+            setDomainConfig();
+            break;
+        case 4:
+            getSSLConfig();
+            active.value++
+            break
+        case 5:
+            setSSLConfig();
             active.value++
             break
     }

+ 6 - 4
server/config/config.dev.json

@@ -1,11 +1,13 @@
 {
-  "domain": "",
-  "webDomain": "",
+  "logLevel": "debug",
+  "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": "",
-  "dbType": "",
+  "dbDSN": "./pmail.db",
+  "dbType": "sqlite",
   "weChatPushAppId": "",
   "weChatPushSecret": "",
   "weChatPushTemplateId": "",

+ 5 - 1
server/config/config.go

@@ -11,9 +11,11 @@ import (
 var IsInit bool
 
 type Config struct {
+	LogLevel             string            `json:"logLevel"`
 	Domain               string            `json:"domain"`
 	WebDomain            string            `json:"webDomain"`
 	DkimPrivateKeyPath   string            `json:"dkimPrivateKeyPath"`
+	SSLType              string            `json:"sslType"` // 0表示自动生成证书,1表示用户上传证书
 	SSLPrivateKeyPath    string            `json:"SSLPrivateKeyPath"`
 	SSLPublicKeyPath     string            `json:"SSLPublicKeyPath"`
 	DbDSN                string            `json:"dbDSN"`
@@ -30,10 +32,12 @@ type Config struct {
 //go:embed tables/*
 var tableConfig embed.FS
 
-const Version = "1.1.0"
+const Version = "2.0.0"
 
 const DBTypeMySQL = "mysql"
 const DBTypeSQLite = "sqlite"
+const SSLTypeAuto = "0" //自动生成证书
+const SSLTypeUser = "1" //用户上传证书
 
 var DBTypes []string = []string{DBTypeMySQL, DBTypeSQLite}
 

+ 6 - 4
server/config/config.json

@@ -1,11 +1,13 @@
 {
-  "domain": "",
-  "webDomain": "",
+  "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": "",
-  "dbType": "",
+  "dbDSN": "./pmail.db",
+  "dbType": "sqlite",
   "weChatPushAppId": "",
   "weChatPushSecret": "",
   "weChatPushTemplateId": "",

+ 0 - 1
server/config/dkim/README

@@ -1 +0,0 @@
-使用[go-msgauth](https://github.com/emersion/go-msgauth)项目的dkim-keygen工具生成公钥和私钥

+ 0 - 2
server/config/tables/mysql/data/user.sql

@@ -1,2 +0,0 @@
-INSERT INTO user (account, name, password) VALUES ('admin', 'admin', 'faddb6ec2efe16116a342f5512583c48');
-

+ 0 - 2
server/config/tables/mysql/data/user_auth.sql

@@ -1,2 +0,0 @@
-INSERT INTO pmail.user_auth (user_id, email_account) VALUES (1, '*');
-

+ 0 - 2
server/config/tables/sqlite/data/user.sql

@@ -1,2 +0,0 @@
-INSERT INTO user (account, name, password) VALUES ('admin', 'admin', 'faddb6ec2efe16116a342f5512583c48');
-

+ 0 - 2
server/config/tables/sqlite/data/user_auth.sql

@@ -1,2 +0,0 @@
-INSERT INTO user_auth (user_id, email_account) VALUES (1, '*');
-

+ 11 - 0
server/controllers/interceptor.go

@@ -0,0 +1,11 @@
+package controllers
+
+import (
+	"net/http"
+	"pmail/config"
+)
+
+func Interceptor(w http.ResponseWriter, r *http.Request) {
+	URL := "https://" + config.Instance.WebDomain + r.URL.Path
+	http.Redirect(w, r, URL, http.StatusMovedPermanently)
+}

+ 2 - 9
server/controllers/login.go

@@ -1,9 +1,7 @@
 package controllers
 
 import (
-	"crypto/md5"
 	"database/sql"
-	"encoding/hex"
 	"encoding/json"
 	log "github.com/sirupsen/logrus"
 	"io"
@@ -14,6 +12,7 @@ import (
 	"pmail/i18n"
 	"pmail/models"
 	"pmail/session"
+	"pmail/utils/password"
 )
 
 type loginRequest struct {
@@ -35,7 +34,7 @@ func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 
 	var user models.User
 
-	encodePwd := md5Encode(md5Encode(reqData.Password+"pmail") + "pmail2023")
+	encodePwd := password.Encode(reqData.Password)
 
 	err = db.Instance.Get(&user, db.WithContext(ctx, "select * from user where account =? and password =?"),
 		reqData.Account, encodePwd)
@@ -51,9 +50,3 @@ func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 		response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "aperror"), "").FPrint(w)
 	}
 }
-
-func md5Encode(str string) string {
-	h := md5.New()
-	h.Write([]byte(str))
-	return hex.EncodeToString(h.Sum(nil))
-}

+ 2 - 1
server/controllers/settings.go

@@ -9,6 +9,7 @@ import (
 	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/i18n"
+	"pmail/utils/password"
 )
 
 type modifyPasswordRequest struct {
@@ -27,7 +28,7 @@ func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request)
 	}
 
 	if retData.Password != "" {
-		encodePwd := md5Encode(md5Encode(retData.Password+"pmail") + "pmail2023")
+		encodePwd := password.Encode(retData.Password)
 
 		_, err := db.Instance.Exec(db.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID)
 		if err != nil {

+ 65 - 12
server/controllers/setup.go

@@ -4,13 +4,23 @@ import (
 	"encoding/json"
 	"io"
 	"net/http"
+	"pmail/config"
 	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/services/setup"
+	"pmail/services/setup/ssl"
+	"strings"
 )
 
-func Proxy(w http.ResponseWriter, r *http.Request) {
-	w.Write([]byte("proxy"))
+func AcmeChallenge(w http.ResponseWriter, r *http.Request) {
+	instance := ssl.GetHttpChallengeInstance()
+	token := strings.ReplaceAll(r.URL.Path, "/.well-known/acme-challenge/", "")
+	auth, exist := instance.AuthInfo[token]
+	if exist {
+		w.Write([]byte(auth.KeyAuth))
+	} else {
+		http.NotFound(w, r)
+	}
 }
 
 func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
@@ -29,9 +39,10 @@ func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 	}
 
 	if reqData["step"] == "database" && reqData["action"] == "get" {
-		dbType, dbDSN, err := setup.GetDatabaseSettings()
+		dbType, dbDSN, err := setup.GetDatabaseSettings(ctx)
 		if err != nil {
-			response.NewErrorResponse(response.ServerError, err.Error(), "")
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
 		}
 
 		response.NewSuccessResponse(map[string]string{
@@ -42,19 +53,41 @@ func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 	}
 
 	if reqData["step"] == "database" && reqData["action"] == "set" {
-		err := setup.SetDatabaseSettings(reqData["db_type"], reqData["db_dsn"])
+		err := setup.SetDatabaseSettings(ctx, reqData["db_type"], reqData["db_dsn"])
 		if err != nil {
-			response.NewErrorResponse(response.ServerError, err.Error(), "")
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
 		}
 
 		response.NewSuccessResponse("Succ").FPrint(w)
 		return
 	}
 
+	if reqData["step"] == "password" && reqData["action"] == "get" {
+		ok, err := setup.GetAdminPassword(ctx)
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+		response.NewSuccessResponse(ok).FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "password" && reqData["action"] == "set" {
+		err := setup.SetAdminPassword(ctx, reqData["account"], reqData["password"])
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+		response.NewSuccessResponse("Succ").FPrint(w)
+		return
+	}
+
 	if reqData["step"] == "domain" && reqData["action"] == "get" {
 		smtpDomain, webDomain, err := setup.GetDomainSettings()
 		if err != nil {
-			response.NewErrorResponse(response.ServerError, err.Error(), "")
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
 		}
 		response.NewSuccessResponse(map[string]string{
 			"smtp_domain": smtpDomain,
@@ -66,7 +99,8 @@ func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 	if reqData["step"] == "domain" && reqData["action"] == "set" {
 		err := setup.SetDomainSettings(reqData["smtp_domain"], reqData["web_domain"])
 		if err != nil {
-			response.NewErrorResponse(response.ServerError, err.Error(), "")
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
 		}
 		response.NewSuccessResponse("Succ").FPrint(w)
 		return
@@ -75,18 +109,37 @@ func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 	if reqData["step"] == "dns" && reqData["action"] == "get" {
 		dnsInfos, err := setup.GetDNSSettings(ctx)
 		if err != nil {
-			response.NewErrorResponse(response.ServerError, err.Error(), "")
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
 		}
 		response.NewSuccessResponse(dnsInfos).FPrint(w)
 		return
 	}
 
 	if reqData["step"] == "ssl" && reqData["action"] == "get" {
-		err := setup.GenSSL()
+		sslType := ssl.GetSSL()
+		response.NewSuccessResponse(sslType).FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "ssl" && reqData["action"] == "set" {
+		err := ssl.SetSSL(reqData["ssl_type"])
 		if err != nil {
-			response.NewErrorResponse(response.ServerError, err.Error(), "")
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
 		}
-		response.NewSuccessResponse("").FPrint(w)
+
+		if reqData["ssl_type"] == config.SSLTypeAuto {
+			err = ssl.GenSSL(false)
+			if err != nil {
+				response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+				return
+			}
+		}
+
+		response.NewSuccessResponse("Succ").FPrint(w)
+		setup.Finish(ctx)
 		return
 	}
+
 }

+ 34 - 0
server/cron_server/ssl_update.go

@@ -0,0 +1,34 @@
+package cron_server
+
+import (
+	log "github.com/sirupsen/logrus"
+	"pmail/config"
+	"pmail/services/setup/ssl"
+	"pmail/signal"
+	"time"
+)
+
+func Start() {
+	for {
+		if config.Instance.IsInit {
+			days, err := ssl.CheckSSLCrtInfo()
+			if days < 30 || err != nil {
+				if err != nil {
+					log.Errorf("SSL Check Error, Update SSL Certificate. Error Info :%+v", err)
+				} else {
+					log.Infof("SSL certificate remaining time is only %d days, renew SSL certificate.", days)
+				}
+				err = ssl.GenSSL(true)
+				if err != nil {
+					log.Errorf("SSL Update Error! %+v", err)
+				}
+				// 更新完证书,重启服务
+				signal.RestartChan <- true
+			} else {
+				log.Debugf("SSL Check.")
+			}
+		}
+		// 每24小时检测一次证书有效期
+		time.Sleep(24 * time.Hour)
+	}
+}

+ 5 - 3
server/db/init.go

@@ -8,11 +8,12 @@ import (
 	_ "modernc.org/sqlite"
 	"pmail/config"
 	"pmail/dto"
+	"pmail/utils/errors"
 )
 
 var Instance *sqlx.DB
 
-func Init() {
+func Init() error {
 	dsn := config.Instance.DbDSN
 	var err error
 
@@ -22,15 +23,16 @@ func Init() {
 	case "sqlite":
 		Instance, err = sqlx.Open("sqlite", dsn)
 	default:
-		return
+		return errors.New("Database Type Error!")
 	}
 	if err != nil {
-		panic(err)
+		return errors.Wrap(err)
 	}
 	Instance.SetMaxOpenConns(100)
 	Instance.SetMaxIdleConns(10)
 	//showMySQLCharacterSet()
 	checkTable()
+	return nil
 }
 
 func WithContext(ctx *dto.Context, sql string) string {

+ 11 - 12
server/hooks/wechat_push/wechat_push.go

@@ -41,6 +41,10 @@ func (w *WeChatPushHook) ReceiveParseBefore(email []byte) {
 }
 
 func (w *WeChatPushHook) ReceiveParseAfter(email *parsemail.Email) {
+	if w.appId == "" || w.secret == "" || w.pushUser == "" {
+		return
+	}
+
 	w.sendUserMsg(nil, w.pushUser, string(email.Text))
 }
 
@@ -91,18 +95,13 @@ func (w *WeChatPushHook) sendUserMsg(ctx *dto.Context, userId string, content st
 
 }
 func NewWechatPushHook() *WeChatPushHook {
-	if config.Instance.WeChatPushAppId != "" &&
-		config.Instance.WeChatPushSecret != "" &&
-		config.Instance.WeChatPushTemplateId != "" &&
-		config.Instance.WeChatPushUserId != "" {
-		ret := &WeChatPushHook{
-			appId:      config.Instance.WeChatPushAppId,
-			secret:     config.Instance.WeChatPushSecret,
-			templateId: config.Instance.WeChatPushTemplateId,
-			pushUser:   config.Instance.WeChatPushUserId,
-		}
-		return ret
 
+	ret := &WeChatPushHook{
+		appId:      config.Instance.WeChatPushAppId,
+		secret:     config.Instance.WeChatPushSecret,
+		templateId: config.Instance.WeChatPushTemplateId,
+		pushUser:   config.Instance.WeChatPushUserId,
 	}
-	return nil
+	return ret
+
 }

+ 35 - 0
server/http_server/http_server.go

@@ -0,0 +1,35 @@
+package http_server
+
+import (
+	"fmt"
+	"net/http"
+	"pmail/controllers"
+	"time"
+)
+
+const HttpPort = 80
+
+// 这个服务是为了拦截http请求转发到https
+var httpServer *http.Server
+
+func HttpStop() {
+	if httpServer != nil {
+		httpServer.Close()
+	}
+}
+
+func HttpStart() {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", controllers.Interceptor)
+	httpServer = &http.Server{
+		Addr:         fmt.Sprintf(":%d", HttpPort),
+		Handler:      mux,
+		ReadTimeout:  time.Second * 60,
+		WriteTimeout: time.Second * 60,
+	}
+
+	err := httpServer.ListenAndServe()
+	if err != nil {
+		panic(err)
+	}
+}

+ 19 - 51
server/http_server/main.go → server/http_server/https_server.go

@@ -9,6 +9,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cast"
 	"io/fs"
+	olog "log"
 	"math/rand"
 	"net"
 	"net/http"
@@ -26,43 +27,19 @@ import (
 //go:embed dist/*
 var local embed.FS
 
-var ip string
+const HttpsPort = 443
 
-const HttpPort = 80
+var httpsServer *http.Server
 
-var setupServer *http.Server
-
-func SetupStart() {
-	mux := http.NewServeMux()
-	fe, err := fs.Sub(local, "dist")
-	if err != nil {
-		panic(err)
-	}
-	mux.Handle("/", http.FileServer(http.FS(fe)))
-	mux.HandleFunc("/api/", contextIterceptor(controllers.Setup))
-	mux.HandleFunc("/", controllers.Proxy)
-
-	setupServer := &http.Server{
-		Addr:         fmt.Sprintf(":%d", HttpPort),
-		Handler:      mux,
-		ReadTimeout:  time.Second * 60,
-		WriteTimeout: time.Second * 60,
-	}
-	err = setupServer.ListenAndServe()
-	if err != nil {
-		panic(err)
-	}
+type nullWrite struct {
 }
 
-func SetupStop() {
-	err := setupServer.Close()
-	if err != nil {
-		panic(err)
-	}
+func (w *nullWrite) Write(p []byte) (int, error) {
+	return len(p), nil
 }
 
-func Start() {
-	log.Infof("Http Server Start at :%d", HttpPort)
+func HttpsStart() {
+	log.Infof("Http Server Start")
 
 	mux := http.NewServeMux()
 
@@ -82,36 +59,27 @@ func Start() {
 	mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
 	mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
 
-	server := &http.Server{
-		Addr:         fmt.Sprintf(":%d", HttpPort),
+	// go http server会打一堆没用的日志,写一个空的日志处理器,屏蔽掉日志输出
+	nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
+
+	httpsServer = &http.Server{
+		Addr:         fmt.Sprintf(":%d", HttpsPort),
 		Handler:      session.Instance.LoadAndSave(mux),
 		ReadTimeout:  time.Second * 60,
 		WriteTimeout: time.Second * 60,
+		ErrorLog:     nullLog,
 	}
 
-	//err := server.ListenAndServeTLS( "config/ssl/public.crt", "config/ssl/private.key", nil)
-	err = server.ListenAndServe()
+	err = httpsServer.ListenAndServeTLS("config/ssl/public.crt", "config/ssl/private.key")
 	if err != nil {
 		panic(err)
 	}
 }
 
-func getLocalIP() string {
-	ip := "127.0.0.1"
-	addrs, err := net.InterfaceAddrs()
-	if err != nil {
-		return ip
+func HttpsStop() {
+	if httpsServer != nil {
+		httpsServer.Close()
 	}
-	for _, a := range addrs {
-		if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
-			if ipnet.IP.To4() != nil {
-				ip = ipnet.IP.String()
-				break
-			}
-		}
-	}
-
-	return ip
 }
 
 func genLogID() string {
@@ -158,7 +126,7 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
 			}
 			if ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
 				if r.URL.Path != "/api/ping" && r.URL.Path != "/api/login" {
-					response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "login_exp"), "").FPrint(w)
+					response.NewErrorResponse(response.NeedLogin, i18n.GetText(ctx.Lang, "login_exp"), "").FPrint(w)
 					return
 				}
 			}

+ 63 - 0
server/http_server/setup_server.go

@@ -0,0 +1,63 @@
+package http_server
+
+import (
+	"fmt"
+	"io/fs"
+	"net"
+	"net/http"
+	"pmail/controllers"
+	"time"
+)
+
+var ip string
+
+// 项目初始化引导用的服务,初始化引导结束后即退出
+var setupServer *http.Server
+
+func SetupStart() {
+	mux := http.NewServeMux()
+	fe, err := fs.Sub(local, "dist")
+	if err != nil {
+		panic(err)
+	}
+	mux.Handle("/", http.FileServer(http.FS(fe)))
+	mux.HandleFunc("/api/", contextIterceptor(controllers.Setup))
+	// 挑战请求类似这样 /.well-known/acme-challenge/QPyMAyaWw9s5JvV1oruyqWHG7OqkHMJEHPoUz2046KM
+	mux.HandleFunc("/.well-known/", controllers.AcmeChallenge)
+
+	setupServer = &http.Server{
+		Addr:         fmt.Sprintf(":%d", HttpPort),
+		Handler:      mux,
+		ReadTimeout:  time.Second * 60,
+		WriteTimeout: time.Second * 60,
+	}
+	err = setupServer.ListenAndServe()
+	if err != nil && err != http.ErrServerClosed {
+		panic(err)
+	}
+}
+
+func SetupStop() {
+	err := setupServer.Close()
+	if err != nil {
+		panic(err)
+	}
+}
+
+func getLocalIP() string {
+	ip := "127.0.0.1"
+	addrs, err := net.InterfaceAddrs()
+	if err != nil {
+		return ip
+	}
+	for _, a := range addrs {
+		if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+			if ipnet.IP.To4() != nil {
+				ip = ipnet.IP.String()
+				break
+			}
+		}
+	}
+
+	return ip
+}

+ 23 - 3
server/main.go

@@ -6,6 +6,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	"os"
 	"pmail/config"
+	"pmail/cron_server"
 	"pmail/dto"
 	"pmail/res_init"
 	"time"
@@ -47,12 +48,25 @@ func main() {
 	// 日志消息输出可以是任意的io.writer类型
 	log.SetOutput(os.Stdout)
 
-	// 设置日志级别为warn以上
-	log.SetLevel(log.DebugLevel)
 	var cst, _ = time.LoadLocation("Asia/Shanghai")
 	time.Local = cst
 
-	res_init.Init()
+	config.Init()
+
+	switch config.Instance.LogLevel {
+	case "":
+		log.SetLevel(log.InfoLevel)
+	case "debug":
+		log.SetLevel(log.DebugLevel)
+	case "info":
+		log.SetLevel(log.InfoLevel)
+	case "warn":
+		log.SetLevel(log.WarnLevel)
+	case "error":
+		log.SetLevel(log.ErrorLevel)
+	default:
+		log.SetLevel(log.InfoLevel)
+	}
 
 	log.Infoln("***************************************************")
 	log.Infof("***\tServer Start Success Version:%s\n", config.Version)
@@ -61,6 +75,12 @@ func main() {
 	log.Infof("***\tBuild GoLang Version: %s ", goVersion)
 	log.Infoln("***************************************************")
 
+	// 定时任务启动
+	go cron_server.Start()
+
+	// 核心服务启动
+	res_init.Init()
+
 	s := make(chan bool)
 	<-s
 }

+ 25 - 7
server/res_init/init.go

@@ -1,6 +1,7 @@
 package res_init
 
 import (
+	log "github.com/sirupsen/logrus"
 	"os"
 	"pmail/config"
 	"pmail/db"
@@ -8,26 +9,43 @@ import (
 	"pmail/hooks"
 	"pmail/http_server"
 	"pmail/session"
+	"pmail/signal"
 	"pmail/smtp_server"
 	"pmail/utils/file"
 )
 
 func Init() {
-	config.Init()
 
-	if config.IsInit {
+	if !config.IsInit {
+		dirInit()
+
+		log.Infof("Please click http://127.0.0.1 to continue.\n")
+		go http_server.SetupStart()
+		<-signal.InitChan
+		http_server.SetupStop()
+	}
+
+	for {
+		config.Init()
 		parsemail.Init()
-		db.Init()
+		err := db.Init()
+		if err != nil {
+			panic(err)
+		}
 		session.Init()
 		hooks.Init()
 		// smtp server start
 		go smtp_server.Start()
 		// http server start
-		go http_server.Start()
-	} else {
-		dirInit()
-		go http_server.SetupStart()
+		go http_server.HttpsStart()
+		go http_server.HttpStart()
+		<-signal.RestartChan
+		log.Infof("Server Restart!")
+		smtp_server.Stop()
+		http_server.HttpsStop()
+		http_server.HttpStop()
 	}
+
 }
 
 func dirInit() {

+ 1 - 1
server/services/auth/auth.go

@@ -51,7 +51,7 @@ func DkimGen() string {
 		err     error
 	)
 
-	privKey, err = rsa.GenerateKey(rand.Reader, 3072)
+	privKey, err = rsa.GenerateKey(rand.Reader, 1024)
 
 	if err != nil {
 		log.Fatalf("Failed to generate key: %v", err)

+ 7 - 0
server/services/auth/auth_test.go

@@ -0,0 +1,7 @@
+package auth
+
+import "testing"
+
+func TestDkimGen(t *testing.T) {
+	DkimGen()
+}

+ 53 - 9
server/services/setup/db.go

@@ -4,22 +4,62 @@ import (
 	"encoding/json"
 	"os"
 	"pmail/config"
+	"pmail/db"
+	"pmail/dto"
+	"pmail/models"
 	"pmail/utils/array"
 	"pmail/utils/errors"
 	"pmail/utils/file"
+	"pmail/utils/password"
 )
 
-func GetDatabaseSettings() (string, string, error) {
-	configData, err := readConfig()
+func GetDatabaseSettings(ctx *dto.Context) (string, string, error) {
+	configData, err := ReadConfig()
 	if err != nil {
 		return "", "", errors.Wrap(err)
 	}
 
+	if configData.DbType == "" && configData.DbDSN == "" {
+		return config.DBTypeSQLite, "./pmail.db", nil
+	}
+
 	return configData.DbType, configData.DbDSN, nil
 }
 
-func SetDatabaseSettings(dbType, dbDSN string) error {
-	configData, err := readConfig()
+func GetAdminPassword(ctx *dto.Context) (string, error) {
+
+	users := []*models.User{}
+	err := db.Instance.Select(&users, "select * from user")
+	if err != nil {
+		return "", errors.Wrap(err)
+	}
+
+	if len(users) > 0 {
+		return users[0].Account, nil
+	}
+
+	return "", nil
+}
+
+func SetAdminPassword(ctx *dto.Context, account, pwd string) error {
+	encodePwd := password.Encode(pwd)
+	res, err := db.Instance.Exec(db.WithContext(ctx, "INSERT INTO user (account, name, password) VALUES (?, 'admin',?)"), account, encodePwd)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	id, err := res.LastInsertId()
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	_, err = db.Instance.Exec(db.WithContext(ctx, "INSERT INTO user_auth (user_id, email_account) VALUES (?, '*')"), id)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	return nil
+}
+
+func SetDatabaseSettings(ctx *dto.Context, dbType, dbDSN string) error {
+	configData, err := ReadConfig()
 	if err != nil {
 		return errors.Wrap(err)
 	}
@@ -31,16 +71,20 @@ func SetDatabaseSettings(dbType, dbDSN string) error {
 	configData.DbType = dbType
 	configData.DbDSN = dbDSN
 
-	// 检查数据库是否能正确连接 todo
-
-	err = writeConfig(configData)
+	err = WriteConfig(configData)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	config.Init()
+	// 检查数据库是否能正确连接
+	err = db.Init()
 	if err != nil {
 		return errors.Wrap(err)
 	}
 	return nil
 }
 
-func writeConfig(cfg *config.Config) error {
+func WriteConfig(cfg *config.Config) error {
 	bytes, _ := json.Marshal(cfg)
 	err := os.WriteFile("./config/config.json", bytes, 0666)
 	if err != nil {
@@ -49,7 +93,7 @@ func writeConfig(cfg *config.Config) error {
 	return nil
 }
 
-func readConfig() (*config.Config, error) {
+func ReadConfig() (*config.Config, error) {
 	configData := config.Config{
 		DkimPrivateKeyPath: "config/dkim/dkim.priv",
 		SSLPrivateKeyPath:  "config/ssl/private.key",

+ 1 - 1
server/services/setup/dns.go

@@ -20,7 +20,7 @@ type DNSItem struct {
 }
 
 func GetDNSSettings(ctx *dto.Context) ([]*DNSItem, error) {
-	configData, err := readConfig()
+	configData, err := ReadConfig()
 	if err != nil {
 		return nil, errors.Wrap(err)
 	}

+ 11 - 3
server/services/setup/domain.go

@@ -5,7 +5,7 @@ import (
 )
 
 func GetDomainSettings() (string, string, error) {
-	configData, err := readConfig()
+	configData, err := ReadConfig()
 	if err != nil {
 		return "", "", errors.Wrap(err)
 	}
@@ -14,17 +14,25 @@ func GetDomainSettings() (string, string, error) {
 }
 
 func SetDomainSettings(smtpDomain, webDomain string) error {
-	configData, err := readConfig()
+	configData, err := ReadConfig()
 	if err != nil {
 		return errors.Wrap(err)
 	}
 
+	if smtpDomain == "" {
+		return errors.New("domain must not empty!")
+	}
+
+	if webDomain == "" {
+		return errors.New("web domain must not empty!")
+	}
+
 	configData.Domain = smtpDomain
 	configData.WebDomain = webDomain
 
 	// 检查域名是否指向本机 todo
 
-	err = writeConfig(configData)
+	err = WriteConfig(configData)
 	if err != nil {
 		return errors.Wrap(err)
 	}

+ 24 - 0
server/services/setup/finish.go

@@ -0,0 +1,24 @@
+package setup
+
+import (
+	"pmail/dto"
+	"pmail/signal"
+	"pmail/utils/errors"
+)
+
+// Finish 标记初始化完成
+func Finish(ctx *dto.Context) error {
+	cfg, err := ReadConfig()
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	cfg.IsInit = true
+
+	err = WriteConfig(cfg)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	// 初始化完成
+	signal.InitChan <- true
+	return nil
+}

+ 0 - 103
server/services/setup/ssl.go

@@ -1,103 +0,0 @@
-package setup
-
-import (
-	"crypto"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
-	"fmt"
-	log "github.com/sirupsen/logrus"
-	"pmail/utils/errors"
-
-	"github.com/go-acme/lego/v4/certcrypto"
-	"github.com/go-acme/lego/v4/certificate"
-	"github.com/go-acme/lego/v4/challenge/http01"
-	"github.com/go-acme/lego/v4/challenge/tlsalpn01"
-	"github.com/go-acme/lego/v4/lego"
-	"github.com/go-acme/lego/v4/registration"
-)
-
-type MyUser struct {
-	Email        string
-	Registration *registration.Resource
-	key          crypto.PrivateKey
-}
-
-func (u *MyUser) GetEmail() string {
-	return u.Email
-}
-func (u MyUser) GetRegistration() *registration.Resource {
-	return u.Registration
-}
-func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
-	return u.key
-}
-
-func GenSSL() error {
-
-	configData, err := readConfig()
-	if err != nil {
-		return errors.Wrap(err)
-	}
-
-	// Create a user. New accounts need an email and private key to start.
-	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	myUser := MyUser{
-		Email: "i@" + configData.Domain,
-		key:   privateKey,
-	}
-
-	config := lego.NewConfig(&myUser)
-
-	config.Certificate.KeyType = certcrypto.RSA2048
-
-	// A client facilitates communication with the CA server.
-	client, err := lego.NewClient(config)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// We specify an HTTP port of 5002 and an TLS port of 5001 on all interfaces
-	// because we aren't running as root and can't bind a listener to port 80 and 443
-	// (used later when we attempt to pass challenges). Keep in mind that you still
-	// need to proxy challenge traffic to port 5002 and 5001.
-	err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5001"))
-	if err != nil {
-		log.Fatal(err)
-	}
-	err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "443"))
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// New users will need to register
-	reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
-	if err != nil {
-		log.Fatal(err)
-	}
-	myUser.Registration = reg
-
-	request := certificate.ObtainRequest{
-		Domains: []string{
-			fmt.Sprintf("smtp.%s", configData.Domain),
-			configData.WebDomain,
-		},
-		Bundle: true,
-	}
-	certificates, err := client.Certificate.Obtain(request)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// Each certificate comes back with the cert bytes, the bytes of the client's
-	// private key, and a certificate URL. SAVE THESE TO DISK.
-	fmt.Printf("%#v\n", certificates)
-
-	// ... all done.
-
-	return nil
-}

+ 37 - 0
server/services/setup/ssl/challenge.go

@@ -0,0 +1,37 @@
+package ssl
+
+type authInfo struct {
+	Domain  string
+	Token   string
+	KeyAuth string
+}
+
+type HttpChallenge struct {
+	AuthInfo map[string]*authInfo
+}
+
+var instance *HttpChallenge
+
+func (h *HttpChallenge) Present(domain, token, keyAuth string) error {
+	h.AuthInfo[token] = &authInfo{
+		Domain:  domain,
+		Token:   token,
+		KeyAuth: keyAuth,
+	}
+
+	return nil
+}
+
+func (h *HttpChallenge) CleanUp(domain, token, keyAuth string) error {
+	delete(h.AuthInfo, token)
+	return nil
+}
+
+func GetHttpChallengeInstance() *HttpChallenge {
+	if instance == nil {
+		instance = &HttpChallenge{
+			AuthInfo: map[string]*authInfo{},
+		}
+	}
+	return instance
+}

+ 172 - 0
server/services/setup/ssl/ssl.go

@@ -0,0 +1,172 @@
+package ssl
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"github.com/go-acme/lego/v4/certificate"
+	"github.com/spf13/cast"
+	"os"
+	"pmail/config"
+	"pmail/services/setup"
+	"pmail/utils/errors"
+	"time"
+
+	"github.com/go-acme/lego/v4/certcrypto"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/registration"
+)
+
+type MyUser struct {
+	Email        string
+	Registration *registration.Resource
+	key          crypto.PrivateKey
+}
+
+func (u *MyUser) GetEmail() string {
+	return u.Email
+}
+func (u MyUser) GetRegistration() *registration.Resource {
+	return u.Registration
+}
+func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
+	return u.key
+}
+
+func GetSSL() string {
+	cfg, err := setup.ReadConfig()
+	if err != nil {
+		panic(err)
+	}
+	if cfg.SSLType == "" {
+		return config.SSLTypeAuto
+	}
+
+	return cfg.SSLType
+}
+
+func SetSSL(sslType string) error {
+	cfg, err := setup.ReadConfig()
+	if err != nil {
+		panic(err)
+	}
+	if sslType == config.SSLTypeAuto || sslType == config.SSLTypeUser {
+		cfg.SSLType = sslType
+	} else {
+		return errors.New("SSL Type Error!")
+	}
+
+	err = setup.WriteConfig(cfg)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	return nil
+}
+
+func GenSSL(update bool) error {
+
+	cfg, err := setup.ReadConfig()
+	if err != nil {
+		panic(err)
+	}
+
+	if !update {
+		privateFile, errpi := os.ReadFile(cfg.SSLPrivateKeyPath)
+		public, errpu := os.ReadFile(cfg.SSLPublicKeyPath)
+		// 当前存在证书数据,就不生成了
+		if errpi == nil && errpu == nil && len(privateFile) > 0 && len(public) > 0 {
+			return nil
+		}
+	}
+
+	// Create a user. New accounts need an email and private key to start.
+	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	myUser := MyUser{
+		Email: "i@" + cfg.Domain,
+		key:   privateKey,
+	}
+
+	config := lego.NewConfig(&myUser)
+
+	config.Certificate.KeyType = certcrypto.RSA2048
+
+	// A client facilitates communication with the CA server.
+	client, err := lego.NewClient(config)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	err = client.Challenge.SetHTTP01Provider(GetHttpChallengeInstance())
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	myUser.Registration = reg
+
+	request := certificate.ObtainRequest{
+		Domains: []string{"smtp." + cfg.Domain, cfg.WebDomain},
+		Bundle:  true,
+	}
+	certificates, err := client.Certificate.Obtain(request)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	err = os.WriteFile("./config/ssl/private.key", certificates.PrivateKey, 0666)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	err = os.WriteFile("./config/ssl/public.crt", certificates.Certificate, 0666)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	err = os.WriteFile("./config/ssl/issuerCert.crt", certificates.IssuerCertificate, 0666)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	return nil
+}
+
+// CheckSSLCrtInfo 返回证书过期剩余天数
+func CheckSSLCrtInfo() (int, error) {
+
+	cfg, err := setup.ReadConfig()
+	if err != nil {
+		panic(err)
+	}
+	// load cert and key by tls.LoadX509KeyPair
+	tlsCert, err := tls.LoadX509KeyPair(cfg.SSLPublicKeyPath, cfg.SSLPrivateKeyPath)
+	if err != nil {
+		return -1, errors.Wrap(err)
+	}
+
+	cert, err := x509.ParseCertificate(tlsCert.Certificate[0])
+
+	if err != nil {
+		return -1, errors.Wrap(err)
+	}
+
+	// 检查过期时间
+	hours := cert.NotAfter.Sub(time.Now()).Hours()
+
+	if hours <= 0 {
+		return -1, errors.New("Certificate has expired")
+	}
+
+	return cast.ToInt(hours / 24), nil
+}

+ 17 - 0
server/services/setup/ssl/ssl_test.go

@@ -0,0 +1,17 @@
+package ssl
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestGenSSL(t *testing.T) {
+	err := GenSSL(false)
+	fmt.Println(err)
+}
+
+func TestGetSSLCrtInfo(t *testing.T) {
+	days, err := CheckSSLCrtInfo()
+
+	fmt.Println(days, err)
+}

+ 4 - 0
server/signal/signal.go

@@ -0,0 +1,4 @@
+package signal
+
+var InitChan = make(chan bool)
+var RestartChan = make(chan bool)

+ 19 - 11
server/smtp_server/main.go

@@ -43,19 +43,21 @@ func (s *Session) Logout() error {
 	return nil
 }
 
+var instance *smtp.Server
+
 func Start() {
 	be := &Backend{}
 
-	s := smtp.NewServer(be)
+	instance = smtp.NewServer(be)
 
-	s.Addr = ":25"
-	s.Domain = config.Instance.Domain
-	s.ReadTimeout = 10 * time.Second
-	s.WriteTimeout = 10 * time.Second
-	s.MaxMessageBytes = 1024 * 1024
-	s.MaxRecipients = 50
+	instance.Addr = ":25"
+	instance.Domain = config.Instance.Domain
+	instance.ReadTimeout = 10 * time.Second
+	instance.WriteTimeout = 10 * time.Second
+	instance.MaxMessageBytes = 1024 * 1024
+	instance.MaxRecipients = 50
 	// force TLS for auth
-	s.AllowInsecureAuth = false
+	instance.AllowInsecureAuth = false
 	// Load the certificate and key
 	cer, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
 	if err != nil {
@@ -63,10 +65,16 @@ func Start() {
 		return
 	}
 	// Configure the TLS support
-	s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
+	instance.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
 
-	log.Println("Starting server at", s.Addr)
-	if err := s.ListenAndServe(); err != nil {
+	log.Println("Starting server at", instance.Addr)
+	if err := instance.ListenAndServe(); err != nil {
 		log.Fatal(err)
 	}
 }
+
+func Stop() {
+	if instance != nil {
+		instance.Close()
+	}
+}

+ 18 - 0
server/utils/password/encode.go

@@ -0,0 +1,18 @@
+package password
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+)
+
+// Encode 对密码两次md5加盐
+func Encode(password string) string {
+	encodePwd := md5Encode(md5Encode(password+"pmail") + "pmail2023")
+	return encodePwd
+}
+
+func md5Encode(str string) string {
+	h := md5.New()
+	h.Write([]byte(str))
+	return hex.EncodeToString(h.Sum(nil))
+}