mirror of
https://github.com/Jinnrry/PMail.git
synced 2025-11-02 12:44:04 +08:00
v2.0
This commit is contained in:
80
README.md
80
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)
|
## [中文文档](./README_CN.md)
|
||||||
|
|
||||||
## Introduction
|
## 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
|
## 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.
|
* 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
|
## 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
|
* The UI is ugly
|
||||||
|
|
||||||
# How to run
|
# 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 |
|
Open `http://127.0.0.1` in your browser or use your server's public IP to visit, then follow the instructions to
|
||||||
|------|----------------------|----------------------|
|
configure.
|
||||||
| 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.
|
|
||||||
|
|
||||||
## 4、Build(or download)
|
## 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.
|
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 :
|
## 7、 WeChat Message Push
|
||||||
```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.
|
|
||||||
|
|
||||||
|
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
|
# For Developer
|
||||||
|
|
||||||
@@ -104,7 +86,3 @@ The code is in `server` folder.
|
|||||||
## Plugin Development
|
## Plugin Development
|
||||||
|
|
||||||
Reference this file. `server/hooks/wechat_push/wechat_push.go`
|
Reference this file. `server/hooks/wechat_push/wechat_push.go`
|
||||||
|
|
||||||
# What's More
|
|
||||||
|
|
||||||
Welcome PR! Welcome Issues! The project need a Logo !
|
|
||||||
86
README_CN.md
86
README_CN.md
@@ -1,7 +1,13 @@
|
|||||||
# PMail
|
# PMail
|
||||||
|
|
||||||
> Welcome PR! Welcome Issues! 目前代码并不稳定,一定记录好日志!丢信或者信件解析错误可以从日志中找出邮件原始内容!
|
> Welcome PR! Welcome Issues! 目前代码并不稳定,一定记录好日志!丢信或者信件解析错误可以从日志中找出邮件原始内容!
|
||||||
|
|
||||||
|
PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱服务器。单文件运行,包含完整的收发邮件服务和Web端邮件管理功能。只需一台服务器、一个域名、一行代码、一分钟部署时间,你就能够搭建出一个自己的域名邮箱。
|
||||||
|
|
||||||
|
目前项目UI设计比较丑陋、UI交互体验较差,欢迎各位UI、设计师、前端提出指导意见。最后,也为这个项目征集一个漂亮可爱的Logo!
|
||||||
|
|
||||||
|
<img src="./docs/cn.gif" alt="Editor" width="800px">
|
||||||
|
|
||||||
## 为什么写这个项目
|
## 为什么写这个项目
|
||||||
|
|
||||||
迫于越来越多的邮件服务商暂停了针对个人的域名邮箱服务(比如QQ邮箱、微软Outlook邮箱),因此考虑自建域名邮箱服务。
|
迫于越来越多的邮件服务商暂停了针对个人的域名邮箱服务(比如QQ邮箱、微软Outlook邮箱),因此考虑自建域名邮箱服务。
|
||||||
@@ -22,6 +28,10 @@
|
|||||||
|
|
||||||
支持dkim、spf校验。正确配置的情况下,Email Test得分10分。
|
支持dkim、spf校验。正确配置的情况下,Email Test得分10分。
|
||||||
|
|
||||||
|
### 4、自动SSL证书
|
||||||
|
|
||||||
|
实现了ACME协议,程序将自动获取并更新Let’s Encrypt证书。
|
||||||
|
|
||||||
## 其他
|
## 其他
|
||||||
|
|
||||||
### 不足
|
### 不足
|
||||||
@@ -35,78 +45,25 @@
|
|||||||
|
|
||||||
# 如何部署
|
# 如何部署
|
||||||
|
|
||||||
## 1、生成DKIM 秘钥
|
## 1、下载文件
|
||||||
|
|
||||||
```
|
[点击这里](https://github.com/Jinnrry/PMail/releases)下载一个与你匹配的程序文件。
|
||||||
go install github.com/emersion/go-msgauth/cmd/dkim-keygen@latest
|
|
||||||
dkim-keygen
|
|
||||||
```
|
|
||||||
执行后将得到`dkim.priv`文件,公钥数据会直接输出
|
|
||||||
|
|
||||||
生成以后将密钥放到`config/dkim`目录中
|
## 2、运行
|
||||||
|
|
||||||
## 2、设置域名DNS
|
双击打开 OR 执行命令运行
|
||||||
|
|
||||||
添加以下记录到你到域名解析中
|
## 3、配置
|
||||||
|
|
||||||
| 类型 | 主机记录 | 记录值 |
|
浏览器打开 `http://127.0.0.1` 或者是用你服务器公网IP访问,然后按提示配置
|
||||||
|-----|---------------------|------------------|
|
|
||||||
| A | smtp | 服务器IP |
|
|
||||||
| MX | _ | smtp.你的域名 |
|
|
||||||
| TXT | _ | v=spf1 a mx ~all |
|
|
||||||
| TXT | default._domainkey | 你生成的DKIM公钥 |
|
|
||||||
|
|
||||||
## 3、申请域名证书
|
## 4、邮箱得分测试
|
||||||
|
|
||||||
准备好 `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、邮箱得分测试
|
|
||||||
|
|
||||||
建议找一下邮箱测试服务(比如[https://www.mail-tester.com/](https://www.mail-tester.com/))进行邮件得分检测,避免自己某些步骤漏配,导致发件进对方垃圾箱。
|
建议找一下邮箱测试服务(比如[https://www.mail-tester.com/](https://www.mail-tester.com/))进行邮件得分检测,避免自己某些步骤漏配,导致发件进对方垃圾箱。
|
||||||
|
|
||||||
## 8、其他说明
|
## 5、微信推送
|
||||||
|
|
||||||
邮件是否进对方垃圾箱与程序无关、与你的服务器IP、服务器域名有关。我自己搭建的服务,测试了收发QQ、Gmail、Outlook、163、126均正常,无任何拦截,且不会进垃圾箱。
|
|
||||||
|
|
||||||
|
打开运行目录下的 `config/config.json`文件,编辑 `weChatPush` 开头的几个配置项,重启服务即可。
|
||||||
|
|
||||||
# 参与开发
|
# 参与开发
|
||||||
|
|
||||||
@@ -124,6 +81,3 @@ MySQL库名必须叫pmail,另外,数据库必须使用utf8_general_ci字符
|
|||||||
|
|
||||||
参考微信推送插件`server/hooks/wechat_push/wechat_push.go`
|
参考微信推送插件`server/hooks/wechat_push/wechat_push.go`
|
||||||
|
|
||||||
# 最后
|
|
||||||
|
|
||||||
欢迎PR! 欢迎Issue!求个Logo!
|
|
||||||
BIN
docs/cn.gif
Normal file
BIN
docs/cn.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 963 KiB |
BIN
docs/en.gif
Normal file
BIN
docs/en.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1004 KiB |
@@ -6,7 +6,7 @@ import lang from '../i18n/i18n';
|
|||||||
//创建axios的一个实例
|
//创建axios的一个实例
|
||||||
var $http = axios.create({
|
var $http = axios.create({
|
||||||
baseURL: import.meta.env.VITE_APP_URL, //接口统一域名
|
baseURL: import.meta.env.VITE_APP_URL, //接口统一域名
|
||||||
timeout: 6000, //设置超时
|
timeout: 60000, //设置超时
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json;charset=UTF-8;',
|
'Content-Type': 'application/json;charset=UTF-8;',
|
||||||
'Lang': lang.lang
|
'Lang': lang.lang
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ var lang = {
|
|||||||
"setDNS": "Set DNS",
|
"setDNS": "Set DNS",
|
||||||
"setSSL": "Set SSL",
|
"setSSL": "Set SSL",
|
||||||
"setDatabase": "Set Database",
|
"setDatabase": "Set Database",
|
||||||
|
"setAdminPassword": "Set Password",
|
||||||
|
"admin_account": "Administrator Account",
|
||||||
"setOther": "Other",
|
"setOther": "Other",
|
||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
@@ -52,7 +54,10 @@ var lang = {
|
|||||||
"smtp_domain": "SMTP Domain",
|
"smtp_domain": "SMTP Domain",
|
||||||
"web_domain": "Web Domain",
|
"web_domain": "Web Domain",
|
||||||
"dns_desc": "Please add the following information to your DNS records",
|
"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设置",
|
"setDNS": "DNS设置",
|
||||||
"setSSL": "SSL设置",
|
"setSSL": "SSL设置",
|
||||||
"setDatabase": "数据库设置",
|
"setDatabase": "数据库设置",
|
||||||
|
"setAdminPassword": "密码设置",
|
||||||
|
"admin_account": "管理员账号",
|
||||||
"setOther": "其他设置",
|
"setOther": "其他设置",
|
||||||
"welcome": "欢迎",
|
"welcome": "欢迎",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
@@ -110,7 +117,11 @@ var zhCN = {
|
|||||||
"domain_desc": "设置你的域名信息。",
|
"domain_desc": "设置你的域名信息。",
|
||||||
"smtp_domain": "SMTP域名地址",
|
"smtp_domain": "SMTP域名地址",
|
||||||
"web_domain": "Web域名地址",
|
"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) {
|
switch (navigator.language) {
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const validateSender = function (rule, value, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkEmail = function (str) {
|
const checkEmail = function (str) {
|
||||||
var re = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
|
var re = /.+@.+\..+/
|
||||||
if (re.test(str)) {
|
if (re.test(str)) {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<el-steps :active="active" align-center finish-status="success" id="status">
|
<el-steps :active="active" align-center finish-status="success" id="status">
|
||||||
<el-step :title="lang.welcome" />
|
<el-step :title="lang.welcome" />
|
||||||
<el-step :title="lang.setDatabase" />
|
<el-step :title="lang.setDatabase" />
|
||||||
|
<el-step :title="lang.setAdminPassword" />
|
||||||
<el-step :title="lang.SetDomail" />
|
<el-step :title="lang.SetDomail" />
|
||||||
<el-step :title="lang.setDNS" />
|
<el-step :title="lang.setDNS" />
|
||||||
<el-step :title="lang.setSSL" />
|
<el-step :title="lang.setSSL" />
|
||||||
<el-step :title="lang.setOther" />
|
|
||||||
</el-steps>
|
</el-steps>
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +48,33 @@
|
|||||||
|
|
||||||
|
|
||||||
<div v-if="active == 2" class="ctn">
|
<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">
|
<div class="desc">
|
||||||
<h2>{{ lang.SetDomail }}</h2>
|
<h2>{{ lang.SetDomail }}</h2>
|
||||||
<!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
|
<!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
|
||||||
@@ -69,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div v-if="active == 3" class="ctn_s">
|
<div v-if="active == 4" class="ctn_s">
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<h2>{{ lang.setDNS }}</h2>
|
<h2>{{ lang.setDNS }}</h2>
|
||||||
<div style="margin-top: 10px;">{{ lang.dns_desc }}</div>
|
<div style="margin-top: 10px;">{{ lang.dns_desc }}</div>
|
||||||
@@ -94,8 +121,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</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-button id="next" style="margin-top: 12px" @click="next">{{ lang.next }}</el-button>
|
<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 v-loading.fullscreen.lock="fullscreenLoading" id="next" style="margin-top: 12px" @click="next">{{
|
||||||
|
lang.next }}</el-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -108,10 +161,16 @@ import { ElMessage } from 'element-plus'
|
|||||||
import router from "@/router"; //根路由对象
|
import router from "@/router"; //根路由对象
|
||||||
import lang from '../i18n/i18n';
|
import lang from '../i18n/i18n';
|
||||||
|
|
||||||
|
const adminSettings = reactive({
|
||||||
|
"account": "admin",
|
||||||
|
"password": "",
|
||||||
|
"password2": "",
|
||||||
|
"hadSeted": false
|
||||||
|
})
|
||||||
|
|
||||||
const dbSettings = reactive({
|
const dbSettings = reactive({
|
||||||
"type": "",
|
"type": "sqlite",
|
||||||
"dsn": "",
|
"dsn": "./pmail.db",
|
||||||
"lable": ""
|
"lable": ""
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -120,12 +179,57 @@ const domainSettings = reactive({
|
|||||||
"smtp_domain": ""
|
"smtp_domain": ""
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sslSettings = reactive({
|
||||||
|
"type": "0",
|
||||||
|
"key_path": "./config/ssl/private.key",
|
||||||
|
"crt_path": "./config/ssl/public.crt"
|
||||||
|
})
|
||||||
|
|
||||||
const active = ref(0)
|
const active = ref(0)
|
||||||
|
const fullscreenLoading = ref(false)
|
||||||
|
|
||||||
|
|
||||||
const dnsInfos = ref([
|
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 = () => {
|
const getDbConfig = () => {
|
||||||
$http.post("/api/setup", { "action": "get", "step": "database" }).then((res) => {
|
$http.post("/api/setup", { "action": "get", "step": "database" }).then((res) => {
|
||||||
if (res.errorNo != 0) {
|
if (res.errorNo != 0) {
|
||||||
@@ -154,7 +258,7 @@ const setDbConfig = () => {
|
|||||||
ElMessage.error(res.errorMsg)
|
ElMessage.error(res.errorMsg)
|
||||||
} else {
|
} else {
|
||||||
active.value++;
|
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 = () => {
|
const setDomainConfig = () => {
|
||||||
$http.post("/api/setup", { "action": "set", "step": "domain", "web_domain": domainSettings.web_domain, "smtp_domain": domainSettings.smtp_domain }).then((res) => {
|
$http.post("/api/setup", { "action": "set", "step": "domain", "web_domain": domainSettings.web_domain, "smtp_domain": domainSettings.smtp_domain }).then((res) => {
|
||||||
if (res.errorNo != 0) {
|
if (res.errorNo != 0) {
|
||||||
@@ -191,9 +323,17 @@ const next = () => {
|
|||||||
setDbConfig();
|
setDbConfig();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
setDomainConfig();
|
setPassword();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
setDomainConfig();
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
getSSLConfig();
|
||||||
|
active.value++
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
setSSLConfig();
|
||||||
active.value++
|
active.value++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"domain": "",
|
"logLevel": "debug",
|
||||||
"webDomain": "",
|
"domain": "domain.com",
|
||||||
|
"webDomain": "mail.domain.com",
|
||||||
"dkimPrivateKeyPath": "config/dkim/dkim.priv",
|
"dkimPrivateKeyPath": "config/dkim/dkim.priv",
|
||||||
|
"sslType": "0",
|
||||||
"SSLPrivateKeyPath": "config/ssl/private.key",
|
"SSLPrivateKeyPath": "config/ssl/private.key",
|
||||||
"SSLPublicKeyPath": "config/ssl/public.crt",
|
"SSLPublicKeyPath": "config/ssl/public.crt",
|
||||||
"dbDSN": "",
|
"dbDSN": "./pmail.db",
|
||||||
"dbType": "",
|
"dbType": "sqlite",
|
||||||
"weChatPushAppId": "",
|
"weChatPushAppId": "",
|
||||||
"weChatPushSecret": "",
|
"weChatPushSecret": "",
|
||||||
"weChatPushTemplateId": "",
|
"weChatPushTemplateId": "",
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import (
|
|||||||
var IsInit bool
|
var IsInit bool
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
LogLevel string `json:"logLevel"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
WebDomain string `json:"webDomain"`
|
WebDomain string `json:"webDomain"`
|
||||||
DkimPrivateKeyPath string `json:"dkimPrivateKeyPath"`
|
DkimPrivateKeyPath string `json:"dkimPrivateKeyPath"`
|
||||||
|
SSLType string `json:"sslType"` // 0表示自动生成证书,1表示用户上传证书
|
||||||
SSLPrivateKeyPath string `json:"SSLPrivateKeyPath"`
|
SSLPrivateKeyPath string `json:"SSLPrivateKeyPath"`
|
||||||
SSLPublicKeyPath string `json:"SSLPublicKeyPath"`
|
SSLPublicKeyPath string `json:"SSLPublicKeyPath"`
|
||||||
DbDSN string `json:"dbDSN"`
|
DbDSN string `json:"dbDSN"`
|
||||||
@@ -30,10 +32,12 @@ type Config struct {
|
|||||||
//go:embed tables/*
|
//go:embed tables/*
|
||||||
var tableConfig embed.FS
|
var tableConfig embed.FS
|
||||||
|
|
||||||
const Version = "1.1.0"
|
const Version = "2.0.0"
|
||||||
|
|
||||||
const DBTypeMySQL = "mysql"
|
const DBTypeMySQL = "mysql"
|
||||||
const DBTypeSQLite = "sqlite"
|
const DBTypeSQLite = "sqlite"
|
||||||
|
const SSLTypeAuto = "0" //自动生成证书
|
||||||
|
const SSLTypeUser = "1" //用户上传证书
|
||||||
|
|
||||||
var DBTypes []string = []string{DBTypeMySQL, DBTypeSQLite}
|
var DBTypes []string = []string{DBTypeMySQL, DBTypeSQLite}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"domain": "",
|
"logLevel": "info",
|
||||||
"webDomain": "",
|
"domain": "domain.com",
|
||||||
|
"webDomain": "mail.domain.com",
|
||||||
"dkimPrivateKeyPath": "config/dkim/dkim.priv",
|
"dkimPrivateKeyPath": "config/dkim/dkim.priv",
|
||||||
|
"sslType": "0",
|
||||||
"SSLPrivateKeyPath": "config/ssl/private.key",
|
"SSLPrivateKeyPath": "config/ssl/private.key",
|
||||||
"SSLPublicKeyPath": "config/ssl/public.crt",
|
"SSLPublicKeyPath": "config/ssl/public.crt",
|
||||||
"dbDSN": "",
|
"dbDSN": "./pmail.db",
|
||||||
"dbType": "",
|
"dbType": "sqlite",
|
||||||
"weChatPushAppId": "",
|
"weChatPushAppId": "",
|
||||||
"weChatPushSecret": "",
|
"weChatPushSecret": "",
|
||||||
"weChatPushTemplateId": "",
|
"weChatPushTemplateId": "",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
使用[go-msgauth](https://github.com/emersion/go-msgauth)项目的dkim-keygen工具生成公钥和私钥
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
INSERT INTO user (account, name, password) VALUES ('admin', 'admin', 'faddb6ec2efe16116a342f5512583c48');
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
INSERT INTO pmail.user_auth (user_id, email_account) VALUES (1, '*');
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
INSERT INTO user (account, name, password) VALUES ('admin', 'admin', 'faddb6ec2efe16116a342f5512583c48');
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
INSERT INTO user_auth (user_id, email_account) VALUES (1, '*');
|
|
||||||
|
|
||||||
|
|||||||
11
server/controllers/interceptor.go
Normal file
11
server/controllers/interceptor.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"io"
|
"io"
|
||||||
@@ -14,6 +12,7 @@ import (
|
|||||||
"pmail/i18n"
|
"pmail/i18n"
|
||||||
"pmail/models"
|
"pmail/models"
|
||||||
"pmail/session"
|
"pmail/session"
|
||||||
|
"pmail/utils/password"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loginRequest struct {
|
type loginRequest struct {
|
||||||
@@ -35,7 +34,7 @@ func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
var user models.User
|
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 =?"),
|
err = db.Instance.Get(&user, db.WithContext(ctx, "select * from user where account =? and password =?"),
|
||||||
reqData.Account, encodePwd)
|
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)
|
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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"pmail/dto"
|
"pmail/dto"
|
||||||
"pmail/dto/response"
|
"pmail/dto/response"
|
||||||
"pmail/i18n"
|
"pmail/i18n"
|
||||||
|
"pmail/utils/password"
|
||||||
)
|
)
|
||||||
|
|
||||||
type modifyPasswordRequest struct {
|
type modifyPasswordRequest struct {
|
||||||
@@ -27,7 +28,7 @@ func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if retData.Password != "" {
|
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)
|
_, err := db.Instance.Exec(db.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,13 +4,23 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"pmail/config"
|
||||||
"pmail/dto"
|
"pmail/dto"
|
||||||
"pmail/dto/response"
|
"pmail/dto/response"
|
||||||
"pmail/services/setup"
|
"pmail/services/setup"
|
||||||
|
"pmail/services/setup/ssl"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Proxy(w http.ResponseWriter, r *http.Request) {
|
func AcmeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("proxy"))
|
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) {
|
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" {
|
if reqData["step"] == "database" && reqData["action"] == "get" {
|
||||||
dbType, dbDSN, err := setup.GetDatabaseSettings()
|
dbType, dbDSN, err := setup.GetDatabaseSettings(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.NewErrorResponse(response.ServerError, err.Error(), "")
|
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.NewSuccessResponse(map[string]string{
|
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" {
|
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 {
|
if err != nil {
|
||||||
response.NewErrorResponse(response.ServerError, err.Error(), "")
|
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.NewSuccessResponse("Succ").FPrint(w)
|
response.NewSuccessResponse("Succ").FPrint(w)
|
||||||
return
|
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" {
|
if reqData["step"] == "domain" && reqData["action"] == "get" {
|
||||||
smtpDomain, webDomain, err := setup.GetDomainSettings()
|
smtpDomain, webDomain, err := setup.GetDomainSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.NewErrorResponse(response.ServerError, err.Error(), "")
|
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
response.NewSuccessResponse(map[string]string{
|
response.NewSuccessResponse(map[string]string{
|
||||||
"smtp_domain": smtpDomain,
|
"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" {
|
if reqData["step"] == "domain" && reqData["action"] == "set" {
|
||||||
err := setup.SetDomainSettings(reqData["smtp_domain"], reqData["web_domain"])
|
err := setup.SetDomainSettings(reqData["smtp_domain"], reqData["web_domain"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.NewErrorResponse(response.ServerError, err.Error(), "")
|
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
response.NewSuccessResponse("Succ").FPrint(w)
|
response.NewSuccessResponse("Succ").FPrint(w)
|
||||||
return
|
return
|
||||||
@@ -75,18 +109,37 @@ func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
|||||||
if reqData["step"] == "dns" && reqData["action"] == "get" {
|
if reqData["step"] == "dns" && reqData["action"] == "get" {
|
||||||
dnsInfos, err := setup.GetDNSSettings(ctx)
|
dnsInfos, err := setup.GetDNSSettings(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.NewErrorResponse(response.ServerError, err.Error(), "")
|
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
response.NewSuccessResponse(dnsInfos).FPrint(w)
|
response.NewSuccessResponse(dnsInfos).FPrint(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if reqData["step"] == "ssl" && reqData["action"] == "get" {
|
if reqData["step"] == "ssl" && reqData["action"] == "get" {
|
||||||
err := setup.GenSSL()
|
sslType := ssl.GetSSL()
|
||||||
if err != nil {
|
response.NewSuccessResponse(sslType).FPrint(w)
|
||||||
response.NewErrorResponse(response.ServerError, err.Error(), "")
|
|
||||||
}
|
|
||||||
response.NewSuccessResponse("").FPrint(w)
|
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
34
server/cron_server/ssl_update.go
Normal file
34
server/cron_server/ssl_update.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,12 @@ import (
|
|||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
"pmail/config"
|
"pmail/config"
|
||||||
"pmail/dto"
|
"pmail/dto"
|
||||||
|
"pmail/utils/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Instance *sqlx.DB
|
var Instance *sqlx.DB
|
||||||
|
|
||||||
func Init() {
|
func Init() error {
|
||||||
dsn := config.Instance.DbDSN
|
dsn := config.Instance.DbDSN
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -22,15 +23,16 @@ func Init() {
|
|||||||
case "sqlite":
|
case "sqlite":
|
||||||
Instance, err = sqlx.Open("sqlite", dsn)
|
Instance, err = sqlx.Open("sqlite", dsn)
|
||||||
default:
|
default:
|
||||||
return
|
return errors.New("Database Type Error!")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return errors.Wrap(err)
|
||||||
}
|
}
|
||||||
Instance.SetMaxOpenConns(100)
|
Instance.SetMaxOpenConns(100)
|
||||||
Instance.SetMaxIdleConns(10)
|
Instance.SetMaxIdleConns(10)
|
||||||
//showMySQLCharacterSet()
|
//showMySQLCharacterSet()
|
||||||
checkTable()
|
checkTable()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithContext(ctx *dto.Context, sql string) string {
|
func WithContext(ctx *dto.Context, sql string) string {
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ func (w *WeChatPushHook) ReceiveParseBefore(email []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *WeChatPushHook) ReceiveParseAfter(email *parsemail.Email) {
|
func (w *WeChatPushHook) ReceiveParseAfter(email *parsemail.Email) {
|
||||||
|
if w.appId == "" || w.secret == "" || w.pushUser == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.sendUserMsg(nil, w.pushUser, string(email.Text))
|
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 {
|
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
server/http_server/http_server.go
Normal file
35
server/http_server/http_server.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
olog "log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -26,43 +27,19 @@ import (
|
|||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
var local embed.FS
|
var local embed.FS
|
||||||
|
|
||||||
var ip string
|
const HttpsPort = 443
|
||||||
|
|
||||||
const HttpPort = 80
|
var httpsServer *http.Server
|
||||||
|
|
||||||
var setupServer *http.Server
|
type nullWrite struct {
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupStop() {
|
func (w *nullWrite) Write(p []byte) (int, error) {
|
||||||
err := setupServer.Close()
|
return len(p), nil
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start() {
|
func HttpsStart() {
|
||||||
log.Infof("Http Server Start at :%d", HttpPort)
|
log.Infof("Http Server Start")
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
@@ -82,36 +59,27 @@ func Start() {
|
|||||||
mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
|
mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
|
||||||
mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
|
mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
|
||||||
|
|
||||||
server := &http.Server{
|
// go http server会打一堆没用的日志,写一个空的日志处理器,屏蔽掉日志输出
|
||||||
Addr: fmt.Sprintf(":%d", HttpPort),
|
nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
|
||||||
|
|
||||||
|
httpsServer = &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", HttpsPort),
|
||||||
Handler: session.Instance.LoadAndSave(mux),
|
Handler: session.Instance.LoadAndSave(mux),
|
||||||
ReadTimeout: time.Second * 60,
|
ReadTimeout: time.Second * 60,
|
||||||
WriteTimeout: time.Second * 60,
|
WriteTimeout: time.Second * 60,
|
||||||
|
ErrorLog: nullLog,
|
||||||
}
|
}
|
||||||
|
|
||||||
//err := server.ListenAndServeTLS( "config/ssl/public.crt", "config/ssl/private.key", nil)
|
err = httpsServer.ListenAndServeTLS("config/ssl/public.crt", "config/ssl/private.key")
|
||||||
err = server.ListenAndServe()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLocalIP() string {
|
func HttpsStop() {
|
||||||
ip := "127.0.0.1"
|
if httpsServer != nil {
|
||||||
addrs, err := net.InterfaceAddrs()
|
httpsServer.Close()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func genLogID() string {
|
func genLogID() string {
|
||||||
@@ -158,7 +126,7 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
if ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
|
if ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
|
||||||
if r.URL.Path != "/api/ping" && r.URL.Path != "/api/login" {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
63
server/http_server/setup_server.go
Normal file
63
server/http_server/setup_server.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"os"
|
"os"
|
||||||
"pmail/config"
|
"pmail/config"
|
||||||
|
"pmail/cron_server"
|
||||||
"pmail/dto"
|
"pmail/dto"
|
||||||
"pmail/res_init"
|
"pmail/res_init"
|
||||||
"time"
|
"time"
|
||||||
@@ -47,12 +48,25 @@ func main() {
|
|||||||
// 日志消息输出可以是任意的io.writer类型
|
// 日志消息输出可以是任意的io.writer类型
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
// 设置日志级别为warn以上
|
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
var cst, _ = time.LoadLocation("Asia/Shanghai")
|
var cst, _ = time.LoadLocation("Asia/Shanghai")
|
||||||
time.Local = cst
|
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.Infoln("***************************************************")
|
||||||
log.Infof("***\tServer Start Success Version:%s\n", config.Version)
|
log.Infof("***\tServer Start Success Version:%s\n", config.Version)
|
||||||
@@ -61,6 +75,12 @@ func main() {
|
|||||||
log.Infof("***\tBuild GoLang Version: %s ", goVersion)
|
log.Infof("***\tBuild GoLang Version: %s ", goVersion)
|
||||||
log.Infoln("***************************************************")
|
log.Infoln("***************************************************")
|
||||||
|
|
||||||
|
// 定时任务启动
|
||||||
|
go cron_server.Start()
|
||||||
|
|
||||||
|
// 核心服务启动
|
||||||
|
res_init.Init()
|
||||||
|
|
||||||
s := make(chan bool)
|
s := make(chan bool)
|
||||||
<-s
|
<-s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package res_init
|
package res_init
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"os"
|
"os"
|
||||||
"pmail/config"
|
"pmail/config"
|
||||||
"pmail/db"
|
"pmail/db"
|
||||||
@@ -8,26 +9,43 @@ import (
|
|||||||
"pmail/hooks"
|
"pmail/hooks"
|
||||||
"pmail/http_server"
|
"pmail/http_server"
|
||||||
"pmail/session"
|
"pmail/session"
|
||||||
|
"pmail/signal"
|
||||||
"pmail/smtp_server"
|
"pmail/smtp_server"
|
||||||
"pmail/utils/file"
|
"pmail/utils/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
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()
|
parsemail.Init()
|
||||||
db.Init()
|
err := db.Init()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
session.Init()
|
session.Init()
|
||||||
hooks.Init()
|
hooks.Init()
|
||||||
// smtp server start
|
// smtp server start
|
||||||
go smtp_server.Start()
|
go smtp_server.Start()
|
||||||
// http server start
|
// http server start
|
||||||
go http_server.Start()
|
go http_server.HttpsStart()
|
||||||
} else {
|
go http_server.HttpStart()
|
||||||
dirInit()
|
<-signal.RestartChan
|
||||||
go http_server.SetupStart()
|
log.Infof("Server Restart!")
|
||||||
|
smtp_server.Stop()
|
||||||
|
http_server.HttpsStop()
|
||||||
|
http_server.HttpStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func dirInit() {
|
func dirInit() {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func DkimGen() string {
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
privKey, err = rsa.GenerateKey(rand.Reader, 3072)
|
privKey, err = rsa.GenerateKey(rand.Reader, 1024)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to generate key: %v", err)
|
log.Fatalf("Failed to generate key: %v", err)
|
||||||
|
|||||||
7
server/services/auth/auth_test.go
Normal file
7
server/services/auth/auth_test.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDkimGen(t *testing.T) {
|
||||||
|
DkimGen()
|
||||||
|
}
|
||||||
@@ -4,22 +4,62 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"pmail/config"
|
"pmail/config"
|
||||||
|
"pmail/db"
|
||||||
|
"pmail/dto"
|
||||||
|
"pmail/models"
|
||||||
"pmail/utils/array"
|
"pmail/utils/array"
|
||||||
"pmail/utils/errors"
|
"pmail/utils/errors"
|
||||||
"pmail/utils/file"
|
"pmail/utils/file"
|
||||||
|
"pmail/utils/password"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetDatabaseSettings() (string, string, error) {
|
func GetDatabaseSettings(ctx *dto.Context) (string, string, error) {
|
||||||
configData, err := readConfig()
|
configData, err := ReadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", errors.Wrap(err)
|
return "", "", errors.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if configData.DbType == "" && configData.DbDSN == "" {
|
||||||
|
return config.DBTypeSQLite, "./pmail.db", nil
|
||||||
|
}
|
||||||
|
|
||||||
return configData.DbType, configData.DbDSN, nil
|
return configData.DbType, configData.DbDSN, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetDatabaseSettings(dbType, dbDSN string) error {
|
func GetAdminPassword(ctx *dto.Context) (string, error) {
|
||||||
configData, err := readConfig()
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err)
|
return errors.Wrap(err)
|
||||||
}
|
}
|
||||||
@@ -31,16 +71,20 @@ func SetDatabaseSettings(dbType, dbDSN string) error {
|
|||||||
configData.DbType = dbType
|
configData.DbType = dbType
|
||||||
configData.DbDSN = dbDSN
|
configData.DbDSN = dbDSN
|
||||||
|
|
||||||
// 检查数据库是否能正确连接 todo
|
err = WriteConfig(configData)
|
||||||
|
if err != nil {
|
||||||
err = writeConfig(configData)
|
return errors.Wrap(err)
|
||||||
|
}
|
||||||
|
config.Init()
|
||||||
|
// 检查数据库是否能正确连接
|
||||||
|
err = db.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err)
|
return errors.Wrap(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeConfig(cfg *config.Config) error {
|
func WriteConfig(cfg *config.Config) error {
|
||||||
bytes, _ := json.Marshal(cfg)
|
bytes, _ := json.Marshal(cfg)
|
||||||
err := os.WriteFile("./config/config.json", bytes, 0666)
|
err := os.WriteFile("./config/config.json", bytes, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,7 +93,7 @@ func writeConfig(cfg *config.Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readConfig() (*config.Config, error) {
|
func ReadConfig() (*config.Config, error) {
|
||||||
configData := config.Config{
|
configData := config.Config{
|
||||||
DkimPrivateKeyPath: "config/dkim/dkim.priv",
|
DkimPrivateKeyPath: "config/dkim/dkim.priv",
|
||||||
SSLPrivateKeyPath: "config/ssl/private.key",
|
SSLPrivateKeyPath: "config/ssl/private.key",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type DNSItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetDNSSettings(ctx *dto.Context) ([]*DNSItem, error) {
|
func GetDNSSettings(ctx *dto.Context) ([]*DNSItem, error) {
|
||||||
configData, err := readConfig()
|
configData, err := ReadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err)
|
return nil, errors.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetDomainSettings() (string, string, error) {
|
func GetDomainSettings() (string, string, error) {
|
||||||
configData, err := readConfig()
|
configData, err := ReadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", errors.Wrap(err)
|
return "", "", errors.Wrap(err)
|
||||||
}
|
}
|
||||||
@@ -14,17 +14,25 @@ func GetDomainSettings() (string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SetDomainSettings(smtpDomain, webDomain string) error {
|
func SetDomainSettings(smtpDomain, webDomain string) error {
|
||||||
configData, err := readConfig()
|
configData, err := ReadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err)
|
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.Domain = smtpDomain
|
||||||
configData.WebDomain = webDomain
|
configData.WebDomain = webDomain
|
||||||
|
|
||||||
// 检查域名是否指向本机 todo
|
// 检查域名是否指向本机 todo
|
||||||
|
|
||||||
err = writeConfig(configData)
|
err = WriteConfig(configData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err)
|
return errors.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|||||||
24
server/services/setup/finish.go
Normal file
24
server/services/setup/finish.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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
server/services/setup/ssl/challenge.go
Normal file
37
server/services/setup/ssl/challenge.go
Normal file
@@ -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
server/services/setup/ssl/ssl.go
Normal file
172
server/services/setup/ssl/ssl.go
Normal file
@@ -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
server/services/setup/ssl/ssl_test.go
Normal file
17
server/services/setup/ssl/ssl_test.go
Normal file
@@ -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
server/signal/signal.go
Normal file
4
server/signal/signal.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package signal
|
||||||
|
|
||||||
|
var InitChan = make(chan bool)
|
||||||
|
var RestartChan = make(chan bool)
|
||||||
@@ -43,19 +43,21 @@ func (s *Session) Logout() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var instance *smtp.Server
|
||||||
|
|
||||||
func Start() {
|
func Start() {
|
||||||
be := &Backend{}
|
be := &Backend{}
|
||||||
|
|
||||||
s := smtp.NewServer(be)
|
instance = smtp.NewServer(be)
|
||||||
|
|
||||||
s.Addr = ":25"
|
instance.Addr = ":25"
|
||||||
s.Domain = config.Instance.Domain
|
instance.Domain = config.Instance.Domain
|
||||||
s.ReadTimeout = 10 * time.Second
|
instance.ReadTimeout = 10 * time.Second
|
||||||
s.WriteTimeout = 10 * time.Second
|
instance.WriteTimeout = 10 * time.Second
|
||||||
s.MaxMessageBytes = 1024 * 1024
|
instance.MaxMessageBytes = 1024 * 1024
|
||||||
s.MaxRecipients = 50
|
instance.MaxRecipients = 50
|
||||||
// force TLS for auth
|
// force TLS for auth
|
||||||
s.AllowInsecureAuth = false
|
instance.AllowInsecureAuth = false
|
||||||
// Load the certificate and key
|
// Load the certificate and key
|
||||||
cer, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
|
cer, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,10 +65,16 @@ func Start() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Configure the TLS support
|
// 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)
|
log.Println("Starting server at", instance.Addr)
|
||||||
if err := s.ListenAndServe(); err != nil {
|
if err := instance.ListenAndServe(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Stop() {
|
||||||
|
if instance != nil {
|
||||||
|
instance.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
18
server/utils/password/encode.go
Normal file
18
server/utils/password/encode.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user