diff --git a/README.md b/README.md
index f2745bc..a6b4a1d 100644
--- a/README.md
+++ b/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!
+
+
## 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
+## 1、Download
-Generate public and private keys by the dkim-keygen tool of the [go-msgauth](https://github.com/emersion/go-msgauth) project
+[Click Here](https://github.com/Jinnrry/PMail/releases) Download a program file that matches you.
-Put the key in the `config/dkim` directory.
+## 2、Run
-## 2、Set DNS
+`double-click to open` Or `execute command to run`
-Add the following records to your domain DNS settings
+## 3、Configuration
-| 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、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:
+## 6、Email Test
-MySQL database name must is `pmail`, and charset must is `utf8_general_ci`.
+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.
-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
-}
-```
-
-## 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 !
\ No newline at end of file
diff --git a/README_CN.md b/README_CN.md
index 40e0612..abd036e 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -1,7 +1,13 @@
-# PMail
+# PMail
> Welcome PR! Welcome Issues! 目前代码并不稳定,一定记录好日志!丢信或者信件解析错误可以从日志中找出邮件原始内容!
+PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱服务器。单文件运行,包含完整的收发邮件服务和Web端邮件管理功能。只需一台服务器、一个域名、一行代码、一分钟部署时间,你就能够搭建出一个自己的域名邮箱。
+
+目前项目UI设计比较丑陋、UI交互体验较差,欢迎各位UI、设计师、前端提出指导意见。最后,也为这个项目征集一个漂亮可爱的Logo!
+
+
+
## 为什么写这个项目
迫于越来越多的邮件服务商暂停了针对个人的域名邮箱服务(比如QQ邮箱、微软Outlook邮箱),因此考虑自建域名邮箱服务。
@@ -22,6 +28,10 @@
支持dkim、spf校验。正确配置的情况下,Email Test得分10分。
+### 4、自动SSL证书
+
+实现了ACME协议,程序将自动获取并更新Let’s Encrypt证书。
+
## 其他
### 不足
@@ -35,78 +45,25 @@
# 如何部署
-## 1、生成DKIM 秘钥
+## 1、下载文件
-```
-go install github.com/emersion/go-msgauth/cmd/dkim-keygen@latest
-dkim-keygen
-```
-执行后将得到`dkim.priv`文件,公钥数据会直接输出
+[点击这里](https://github.com/Jinnrry/PMail/releases)下载一个与你匹配的程序文件。
-生成以后将密钥放到`config/dkim`目录中
+## 2、运行
-## 2、设置域名DNS
+双击打开 OR 执行命令运行
-添加以下记录到你到域名解析中
+## 3、配置
-| 类型 | 主机记录 | 记录值 |
-|-----|---------------------|------------------|
-| A | smtp | 服务器IP |
-| MX | _ | smtp.你的域名 |
-| TXT | _ | v=spf1 a mx ~all |
-| TXT | default._domainkey | 你生成的DKIM公钥 |
+浏览器打开 `http://127.0.0.1` 或者是用你服务器公网IP访问,然后按提示配置
-## 3、申请域名证书
-
-准备好 `smtp.你的域名` 的证书,key格式的私钥和crt格式的公钥
-
-放到`config/ssl`目录中
-
-## 4、编译程序(或者直接下载编译好的二进制文件)
-
-1、前端环境:安装好node环境,配置好yarn
-
-2、后端环境:安装最新的golang
-
-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!
\ No newline at end of file
diff --git a/build.sh b/build.sh
old mode 100755
new mode 100644
diff --git a/docs/cn.gif b/docs/cn.gif
new file mode 100644
index 0000000..05e8f7d
Binary files /dev/null and b/docs/cn.gif differ
diff --git a/docs/en.gif b/docs/en.gif
new file mode 100644
index 0000000..3f9e45c
Binary files /dev/null and b/docs/en.gif differ
diff --git a/fe/src/http/http.js b/fe/src/http/http.js
index e7ffd82..a326fe5 100644
--- a/fe/src/http/http.js
+++ b/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
diff --git a/fe/src/i18n/i18n.js b/fe/src/i18n/i18n.js
index 72f4c17..c2e72af 100644
--- a/fe/src/i18n/i18n.js
+++ b/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) {
diff --git a/fe/src/views/EditerView.vue b/fe/src/views/EditerView.vue
index 43404e5..c043f88 100644
--- a/fe/src/views/EditerView.vue
+++ b/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 {
diff --git a/fe/src/views/SetupView.vue b/fe/src/views/SetupView.vue
index 9ae409f..e7a738f 100644
--- a/fe/src/views/SetupView.vue
+++ b/fe/src/views/SetupView.vue
@@ -3,10 +3,10 @@
+
-
@@ -48,6 +48,33 @@
{{ lang.SetDomail }}
@@ -69,7 +96,7 @@
-
+
{{ lang.setDNS }}
{{ lang.dns_desc }}
@@ -94,8 +121,34 @@
+
+
+
{{ lang.setSSL }}
+
{{ lang.setSSL }}
+
+
+
+
+
+
+
+
+
- {{ lang.next }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{
+ lang.next }}
@@ -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
}
diff --git a/server/config/config.dev.json b/server/config/config.dev.json
index f4b3891..da53b9a 100644
--- a/server/config/config.dev.json
+++ b/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": "",
diff --git a/server/config/config.go b/server/config/config.go
index a9b405b..2f496b9 100644
--- a/server/config/config.go
+++ b/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}
diff --git a/server/config/config.json b/server/config/config.json
index f4b3891..540a6ef 100644
--- a/server/config/config.json
+++ b/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": "",
diff --git a/server/config/dkim/README b/server/config/dkim/README
deleted file mode 100644
index 4578ca7..0000000
--- a/server/config/dkim/README
+++ /dev/null
@@ -1 +0,0 @@
-使用[go-msgauth](https://github.com/emersion/go-msgauth)项目的dkim-keygen工具生成公钥和私钥
\ No newline at end of file
diff --git a/server/config/tables/mysql/data/user.sql b/server/config/tables/mysql/data/user.sql
index b81caae..e69de29 100644
--- a/server/config/tables/mysql/data/user.sql
+++ b/server/config/tables/mysql/data/user.sql
@@ -1,2 +0,0 @@
-INSERT INTO user (account, name, password) VALUES ('admin', 'admin', 'faddb6ec2efe16116a342f5512583c48');
-
diff --git a/server/config/tables/mysql/data/user_auth.sql b/server/config/tables/mysql/data/user_auth.sql
index e44084d..e69de29 100644
--- a/server/config/tables/mysql/data/user_auth.sql
+++ b/server/config/tables/mysql/data/user_auth.sql
@@ -1,2 +0,0 @@
-INSERT INTO pmail.user_auth (user_id, email_account) VALUES (1, '*');
-
diff --git a/server/config/tables/sqlite/data/user.sql b/server/config/tables/sqlite/data/user.sql
index b81caae..e69de29 100644
--- a/server/config/tables/sqlite/data/user.sql
+++ b/server/config/tables/sqlite/data/user.sql
@@ -1,2 +0,0 @@
-INSERT INTO user (account, name, password) VALUES ('admin', 'admin', 'faddb6ec2efe16116a342f5512583c48');
-
diff --git a/server/config/tables/sqlite/data/user_auth.sql b/server/config/tables/sqlite/data/user_auth.sql
index 7b5228e..e69de29 100644
--- a/server/config/tables/sqlite/data/user_auth.sql
+++ b/server/config/tables/sqlite/data/user_auth.sql
@@ -1,2 +0,0 @@
-INSERT INTO user_auth (user_id, email_account) VALUES (1, '*');
-
diff --git a/server/controllers/interceptor.go b/server/controllers/interceptor.go
new file mode 100644
index 0000000..ad67bd1
--- /dev/null
+++ b/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)
+}
diff --git a/server/controllers/login.go b/server/controllers/login.go
index 6c5a5a7..e908363 100644
--- a/server/controllers/login.go
+++ b/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))
-}
diff --git a/server/controllers/settings.go b/server/controllers/settings.go
index 47297c8..79ca661 100644
--- a/server/controllers/settings.go
+++ b/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 {
diff --git a/server/controllers/setup.go b/server/controllers/setup.go
index 6eaebd8..833cfed 100644
--- a/server/controllers/setup.go
+++ b/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()
- if err != nil {
- response.NewErrorResponse(response.ServerError, err.Error(), "")
- }
- response.NewSuccessResponse("").FPrint(w)
+ 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(), "").FPrint(w)
+ return
+ }
+
+ 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
+ }
+
}
diff --git a/server/cron_server/ssl_update.go b/server/cron_server/ssl_update.go
new file mode 100644
index 0000000..d6a2868
--- /dev/null
+++ b/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)
+ }
+}
diff --git a/server/db/init.go b/server/db/init.go
index 766cab1..dcb1efc 100644
--- a/server/db/init.go
+++ b/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 {
diff --git a/server/hooks/wechat_push/wechat_push.go b/server/hooks/wechat_push/wechat_push.go
index 26c888f..b222ac6 100644
--- a/server/hooks/wechat_push/wechat_push.go
+++ b/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
+
}
diff --git a/server/http_server/http_server.go b/server/http_server/http_server.go
new file mode 100644
index 0000000..a83139e
--- /dev/null
+++ b/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)
+ }
+}
diff --git a/server/http_server/main.go b/server/http_server/https_server.go
similarity index 68%
rename from server/http_server/main.go
rename to server/http_server/https_server.go
index a644d81..f8130f4 100644
--- a/server/http_server/main.go
+++ b/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
}
}
diff --git a/server/http_server/setup_server.go b/server/http_server/setup_server.go
new file mode 100644
index 0000000..f3ae900
--- /dev/null
+++ b/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
+}
diff --git a/server/main.go b/server/main.go
index b874865..81615c1 100644
--- a/server/main.go
+++ b/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
}
diff --git a/server/res_init/init.go b/server/res_init/init.go
index 7dd12d0..0043ae2 100644
--- a/server/res_init/init.go
+++ b/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() {
diff --git a/server/services/auth/auth.go b/server/services/auth/auth.go
index cad1076..a6d92b7 100644
--- a/server/services/auth/auth.go
+++ b/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)
diff --git a/server/services/auth/auth_test.go b/server/services/auth/auth_test.go
new file mode 100644
index 0000000..eff72ce
--- /dev/null
+++ b/server/services/auth/auth_test.go
@@ -0,0 +1,7 @@
+package auth
+
+import "testing"
+
+func TestDkimGen(t *testing.T) {
+ DkimGen()
+}
diff --git a/server/services/setup/db.go b/server/services/setup/db.go
index dfd3541..68e189a 100644
--- a/server/services/setup/db.go
+++ b/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",
diff --git a/server/services/setup/dns.go b/server/services/setup/dns.go
index 8fbbbcf..c1f0d60 100644
--- a/server/services/setup/dns.go
+++ b/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)
}
diff --git a/server/services/setup/domain.go b/server/services/setup/domain.go
index 6d2bafc..9d41fa3 100644
--- a/server/services/setup/domain.go
+++ b/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)
}
diff --git a/server/services/setup/finish.go b/server/services/setup/finish.go
new file mode 100644
index 0000000..aecb2bb
--- /dev/null
+++ b/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
+}
diff --git a/server/services/setup/ssl.go b/server/services/setup/ssl.go
deleted file mode 100644
index 546dcde..0000000
--- a/server/services/setup/ssl.go
+++ /dev/null
@@ -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
-}
diff --git a/server/services/setup/ssl/challenge.go b/server/services/setup/ssl/challenge.go
new file mode 100644
index 0000000..f7aefac
--- /dev/null
+++ b/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
+}
diff --git a/server/services/setup/ssl/ssl.go b/server/services/setup/ssl/ssl.go
new file mode 100644
index 0000000..b4fef3e
--- /dev/null
+++ b/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
+}
diff --git a/server/services/setup/ssl/ssl_test.go b/server/services/setup/ssl/ssl_test.go
new file mode 100644
index 0000000..e0213b6
--- /dev/null
+++ b/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)
+}
diff --git a/server/signal/signal.go b/server/signal/signal.go
new file mode 100644
index 0000000..dc3887d
--- /dev/null
+++ b/server/signal/signal.go
@@ -0,0 +1,4 @@
+package signal
+
+var InitChan = make(chan bool)
+var RestartChan = make(chan bool)
diff --git a/server/smtp_server/main.go b/server/smtp_server/main.go
index c692f19..c313709 100644
--- a/server/smtp_server/main.go
+++ b/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()
+ }
+}
diff --git a/server/utils/password/encode.go b/server/utils/password/encode.go
new file mode 100644
index 0000000..3b5b567
--- /dev/null
+++ b/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))
+}