mirror of
https://github.com/Jinnrry/PMail.git
synced 2025-02-20 11:43:09 +08:00
v2.0
This commit is contained in:
parent
4a26f52fde
commit
318fea24ad
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)
|
||||
|
||||
## Introduction
|
||||
|
||||
An extremely lightweight mailbox server designed for personal use scenarios.
|
||||
PMail is a personal email server that pursues a minimal deployment process and extreme resource consumption. It runs on
|
||||
a single file and contains complete send/receive mail service and web-side mail management functions. Just a server , a
|
||||
domain name , a line of code , a minute of deployment time , you will be able to build a domain name mailbox of your
|
||||
own .
|
||||
|
||||
Any project related Issue, PR is welcome.At present, the project UI design is ugly, UI interaction experience is poor,
|
||||
welcome all UI, designers, front-end guidance. Finally, also for this project to solicit a beautiful and lovely Logo!
|
||||
|
||||
<img src="./docs/en.gif" alt="Editor" width="800px">
|
||||
|
||||
## Features
|
||||
|
||||
@ -16,36 +25,29 @@ An extremely lightweight mailbox server designed for personal use scenarios.
|
||||
|
||||
* Support dkim, spf checksum, [Email Test](https://www.mail-tester.com/) score 10 points if correctly configured.
|
||||
|
||||
* Implementing the ACME protocol, the program will automatically obtain and update Let's Encrypt certificates.
|
||||
|
||||
## Disadvantages
|
||||
|
||||
* At present, only the core function of sending and receiving emails has been completed. Basically, it can only be used by a single person, and does not deal with issues related to permission management in the process of multiple users.
|
||||
* At present, only the core function of sending and receiving emails has been completed. Basically, it can only be used
|
||||
by a single person, and does not deal with issues related to permission management in the process of multiple users.
|
||||
|
||||
* The UI is ugly
|
||||
|
||||
# How to run
|
||||
|
||||
## 1、Generate DKIM secret key
|
||||
## 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 !
|
86
README_CN.md
86
README_CN.md
@ -1,7 +1,13 @@
|
||||
# PMail
|
||||
# PMail
|
||||
|
||||
> Welcome PR! Welcome Issues! 目前代码并不稳定,一定记录好日志!丢信或者信件解析错误可以从日志中找出邮件原始内容!
|
||||
|
||||
PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱服务器。单文件运行,包含完整的收发邮件服务和Web端邮件管理功能。只需一台服务器、一个域名、一行代码、一分钟部署时间,你就能够搭建出一个自己的域名邮箱。
|
||||
|
||||
目前项目UI设计比较丑陋、UI交互体验较差,欢迎各位UI、设计师、前端提出指导意见。最后,也为这个项目征集一个漂亮可爱的Logo!
|
||||
|
||||
<img src="./docs/cn.gif" alt="Editor" width="800px">
|
||||
|
||||
## 为什么写这个项目
|
||||
|
||||
迫于越来越多的邮件服务商暂停了针对个人的域名邮箱服务(比如QQ邮箱、微软Outlook邮箱),因此考虑自建域名邮箱服务。
|
||||
@ -22,6 +28,10 @@
|
||||
|
||||
支持dkim、spf校验。正确配置的情况下,Email Test得分10分。
|
||||
|
||||
### 4、自动SSL证书
|
||||
|
||||
实现了ACME协议,程序将自动获取并更新Let’s Encrypt证书。
|
||||
|
||||
## 其他
|
||||
|
||||
### 不足
|
||||
@ -35,78 +45,25 @@
|
||||
|
||||
# 如何部署
|
||||
|
||||
## 1、生成DKIM 秘钥
|
||||
## 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!
|
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的一个实例
|
||||
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
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -3,10 +3,10 @@
|
||||
<el-steps :active="active" align-center finish-status="success" id="status">
|
||||
<el-step :title="lang.welcome" />
|
||||
<el-step :title="lang.setDatabase" />
|
||||
<el-step :title="lang.setAdminPassword" />
|
||||
<el-step :title="lang.SetDomail" />
|
||||
<el-step :title="lang.setDNS" />
|
||||
<el-step :title="lang.setSSL" />
|
||||
<el-step :title="lang.setOther" />
|
||||
</el-steps>
|
||||
|
||||
|
||||
@ -48,6 +48,33 @@
|
||||
|
||||
|
||||
<div v-if="active == 2" class="ctn">
|
||||
<div class="desc">
|
||||
<h2>{{ lang.setAdminPassword }}</h2>
|
||||
<!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
|
||||
</div>
|
||||
<div class="form" style="width: 400px;">
|
||||
<el-form label-width="120px">
|
||||
|
||||
<el-form-item :label="lang.admin_account">
|
||||
<el-input v-bind:disabled="adminSettings.hadSeted" placeholder="admin"
|
||||
v-model="adminSettings.account"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="lang.password">
|
||||
<el-input type="password" v-bind:disabled="adminSettings.hadSeted" placeholder=""
|
||||
v-model="adminSettings.password"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="lang.enter_again">
|
||||
<el-input type="password" v-bind:disabled="adminSettings.hadSeted" placeholder=""
|
||||
v-model="adminSettings.password2"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="active == 3" class="ctn">
|
||||
<div class="desc">
|
||||
<h2>{{ lang.SetDomail }}</h2>
|
||||
<!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
|
||||
@ -69,7 +96,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="active == 3" class="ctn_s">
|
||||
<div v-if="active == 4" class="ctn_s">
|
||||
<div class="desc">
|
||||
<h2>{{ lang.setDNS }}</h2>
|
||||
<div style="margin-top: 10px;">{{ lang.dns_desc }}</div>
|
||||
@ -94,8 +121,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="active == 5" class="ctn">
|
||||
<div class="desc">
|
||||
<h2>{{ lang.setSSL }}</h2>
|
||||
<div style="margin-top: 10px;">{{ lang.setSSL }}</div>
|
||||
</div>
|
||||
<div class="form" width="600px">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item :label="lang.type">
|
||||
<el-select :placeholder="lang.ssl_auto" v-model="sslSettings.type">
|
||||
<el-option :label="lang.ssl_auto" value="0" />
|
||||
<el-option :label="lang.ssl_manuallyf" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-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>
|
||||
</template>
|
||||
@ -108,10 +161,16 @@ import { ElMessage } from 'element-plus'
|
||||
import router from "@/router"; //根路由对象
|
||||
import lang from '../i18n/i18n';
|
||||
|
||||
const adminSettings = reactive({
|
||||
"account": "admin",
|
||||
"password": "",
|
||||
"password2": "",
|
||||
"hadSeted": false
|
||||
})
|
||||
|
||||
const dbSettings = reactive({
|
||||
"type": "",
|
||||
"dsn": "",
|
||||
"type": "sqlite",
|
||||
"dsn": "./pmail.db",
|
||||
"lable": ""
|
||||
})
|
||||
|
||||
@ -120,12 +179,57 @@ const domainSettings = reactive({
|
||||
"smtp_domain": ""
|
||||
})
|
||||
|
||||
const sslSettings = reactive({
|
||||
"type": "0",
|
||||
"key_path": "./config/ssl/private.key",
|
||||
"crt_path": "./config/ssl/public.crt"
|
||||
})
|
||||
|
||||
const active = ref(0)
|
||||
const fullscreenLoading = ref(false)
|
||||
|
||||
|
||||
const dnsInfos = ref([
|
||||
{ "host": "smtp", "type": "A", "value": "YouServerIp", "prid": "NA", "ttl": "3600" }
|
||||
])
|
||||
|
||||
const setPassword = () => {
|
||||
if (adminSettings.hadSeted) {
|
||||
active.value++;
|
||||
getDomainConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
if (adminSettings.password != adminSettings.password2) {
|
||||
ElMessage.error(lang.err_pwd_diff)
|
||||
} else {
|
||||
$http.post("/api/setup", { "action": "set", "step": "password", "account": adminSettings.account, "password": adminSettings.password }).then((res) => {
|
||||
if (res.errorNo != 0) {
|
||||
ElMessage.error(res.errorMsg)
|
||||
} else {
|
||||
active.value++;
|
||||
getDomainConfig();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getPassword = () => {
|
||||
$http.post("/api/setup", { "action": "get", "step": "password" }).then((res) => {
|
||||
if (res.errorNo != 0) {
|
||||
ElMessage.error(res.errorMsg)
|
||||
} else {
|
||||
adminSettings.hadSeted = res.data != ""
|
||||
if (adminSettings.hadSeted) {
|
||||
adminSettings.account = res.data
|
||||
adminSettings.password = "*******"
|
||||
adminSettings.password2 = "*******"
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getDbConfig = () => {
|
||||
$http.post("/api/setup", { "action": "get", "step": "database" }).then((res) => {
|
||||
if (res.errorNo != 0) {
|
||||
@ -154,7 +258,7 @@ const setDbConfig = () => {
|
||||
ElMessage.error(res.errorMsg)
|
||||
} else {
|
||||
active.value++;
|
||||
getDomainConfig();
|
||||
getPassword();
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -169,6 +273,34 @@ const getDNSConfig = () => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getSSLConfig = () => {
|
||||
$http.post("/api/setup", { "action": "get", "step": "ssl" }).then((res) => {
|
||||
if (res.errorNo != 0) {
|
||||
ElMessage.error(res.errorMsg)
|
||||
} else {
|
||||
sslSettings.type = res.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const setSSLConfig = () => {
|
||||
fullscreenLoading.value = true;
|
||||
$http.post("/api/setup", { "action": "set", "step": "ssl", "ssl_type": sslSettings.type, "key_path": sslSettings.key_path, "crt_path": sslSettings.crt_path }).then((res) => {
|
||||
if (res.errorNo != 0) {
|
||||
fullscreenLoading.value = false;
|
||||
ElMessage.error(res.errorMsg)
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
window.location.href = "https://" + domainSettings.web_domain;
|
||||
}, 10000);
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const setDomainConfig = () => {
|
||||
$http.post("/api/setup", { "action": "set", "step": "domain", "web_domain": domainSettings.web_domain, "smtp_domain": domainSettings.smtp_domain }).then((res) => {
|
||||
if (res.errorNo != 0) {
|
||||
@ -191,9 +323,17 @@ const next = () => {
|
||||
setDbConfig();
|
||||
break;
|
||||
case 2:
|
||||
setDomainConfig();
|
||||
setPassword();
|
||||
break;
|
||||
case 3:
|
||||
setDomainConfig();
|
||||
break;
|
||||
case 4:
|
||||
getSSLConfig();
|
||||
active.value++
|
||||
break
|
||||
case 5:
|
||||
setSSLConfig();
|
||||
active.value++
|
||||
break
|
||||
}
|
||||
|
@ -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": "",
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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": "",
|
||||
|
@ -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
|
||||
|
||||
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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
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"
|
||||
"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 {
|
||||
|
@ -41,6 +41,10 @@ func (w *WeChatPushHook) ReceiveParseBefore(email []byte) {
|
||||
}
|
||||
|
||||
func (w *WeChatPushHook) ReceiveParseAfter(email *parsemail.Email) {
|
||||
if w.appId == "" || w.secret == "" || w.pushUser == "" {
|
||||
return
|
||||
}
|
||||
|
||||
w.sendUserMsg(nil, w.pushUser, string(email.Text))
|
||||
}
|
||||
|
||||
@ -91,18 +95,13 @@ func (w *WeChatPushHook) sendUserMsg(ctx *dto.Context, userId string, content st
|
||||
|
||||
}
|
||||
func NewWechatPushHook() *WeChatPushHook {
|
||||
if config.Instance.WeChatPushAppId != "" &&
|
||||
config.Instance.WeChatPushSecret != "" &&
|
||||
config.Instance.WeChatPushTemplateId != "" &&
|
||||
config.Instance.WeChatPushUserId != "" {
|
||||
ret := &WeChatPushHook{
|
||||
appId: config.Instance.WeChatPushAppId,
|
||||
secret: config.Instance.WeChatPushSecret,
|
||||
templateId: config.Instance.WeChatPushTemplateId,
|
||||
pushUser: config.Instance.WeChatPushUserId,
|
||||
}
|
||||
return ret
|
||||
|
||||
ret := &WeChatPushHook{
|
||||
appId: config.Instance.WeChatPushAppId,
|
||||
secret: config.Instance.WeChatPushSecret,
|
||||
templateId: config.Instance.WeChatPushTemplateId,
|
||||
pushUser: config.Instance.WeChatPushUserId,
|
||||
}
|
||||
return nil
|
||||
return ret
|
||||
|
||||
}
|
||||
|
35
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"
|
||||
"github.com/spf13/cast"
|
||||
"io/fs"
|
||||
olog "log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -26,43 +27,19 @@ import (
|
||||
//go:embed dist/*
|
||||
var local embed.FS
|
||||
|
||||
var ip string
|
||||
const HttpsPort = 443
|
||||
|
||||
const HttpPort = 80
|
||||
var httpsServer *http.Server
|
||||
|
||||
var setupServer *http.Server
|
||||
|
||||
func SetupStart() {
|
||||
mux := http.NewServeMux()
|
||||
fe, err := fs.Sub(local, "dist")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mux.Handle("/", http.FileServer(http.FS(fe)))
|
||||
mux.HandleFunc("/api/", contextIterceptor(controllers.Setup))
|
||||
mux.HandleFunc("/", controllers.Proxy)
|
||||
|
||||
setupServer := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", HttpPort),
|
||||
Handler: mux,
|
||||
ReadTimeout: time.Second * 60,
|
||||
WriteTimeout: time.Second * 60,
|
||||
}
|
||||
err = setupServer.ListenAndServe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
type nullWrite struct {
|
||||
}
|
||||
|
||||
func SetupStop() {
|
||||
err := setupServer.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
func (w *nullWrite) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func Start() {
|
||||
log.Infof("Http Server Start at :%d", HttpPort)
|
||||
func HttpsStart() {
|
||||
log.Infof("Http Server Start")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@ -82,36 +59,27 @@ func Start() {
|
||||
mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
|
||||
mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", HttpPort),
|
||||
// go http server会打一堆没用的日志,写一个空的日志处理器,屏蔽掉日志输出
|
||||
nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
|
||||
|
||||
httpsServer = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", HttpsPort),
|
||||
Handler: session.Instance.LoadAndSave(mux),
|
||||
ReadTimeout: time.Second * 60,
|
||||
WriteTimeout: time.Second * 60,
|
||||
ErrorLog: nullLog,
|
||||
}
|
||||
|
||||
//err := server.ListenAndServeTLS( "config/ssl/public.crt", "config/ssl/private.key", nil)
|
||||
err = server.ListenAndServe()
|
||||
err = httpsServer.ListenAndServeTLS("config/ssl/public.crt", "config/ssl/private.key")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalIP() string {
|
||||
ip := "127.0.0.1"
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ip
|
||||
func HttpsStop() {
|
||||
if httpsServer != nil {
|
||||
httpsServer.Close()
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
ip = ipnet.IP.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
func genLogID() string {
|
||||
@ -158,7 +126,7 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
if ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
|
||||
if r.URL.Path != "/api/ping" && r.URL.Path != "/api/login" {
|
||||
response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "login_exp"), "").FPrint(w)
|
||||
response.NewErrorResponse(response.NeedLogin, i18n.GetText(ctx.Lang, "login_exp"), "").FPrint(w)
|
||||
return
|
||||
}
|
||||
}
|
63
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"
|
||||
"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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -51,7 +51,7 @@ func DkimGen() string {
|
||||
err error
|
||||
)
|
||||
|
||||
privKey, err = rsa.GenerateKey(rand.Reader, 3072)
|
||||
privKey, err = rsa.GenerateKey(rand.Reader, 1024)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate key: %v", err)
|
||||
|
7
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"
|
||||
"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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
func GetDomainSettings() (string, string, error) {
|
||||
configData, err := readConfig()
|
||||
configData, err := ReadConfig()
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err)
|
||||
}
|
||||
@ -14,17 +14,25 @@ func GetDomainSettings() (string, string, error) {
|
||||
}
|
||||
|
||||
func SetDomainSettings(smtpDomain, webDomain string) error {
|
||||
configData, err := readConfig()
|
||||
configData, err := ReadConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
if smtpDomain == "" {
|
||||
return errors.New("domain must not empty!")
|
||||
}
|
||||
|
||||
if webDomain == "" {
|
||||
return errors.New("web domain must not empty!")
|
||||
}
|
||||
|
||||
configData.Domain = smtpDomain
|
||||
configData.WebDomain = webDomain
|
||||
|
||||
// 检查域名是否指向本机 todo
|
||||
|
||||
err = writeConfig(configData)
|
||||
err = WriteConfig(configData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
24
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
|
||||
}
|
||||
|
||||
var instance *smtp.Server
|
||||
|
||||
func Start() {
|
||||
be := &Backend{}
|
||||
|
||||
s := smtp.NewServer(be)
|
||||
instance = smtp.NewServer(be)
|
||||
|
||||
s.Addr = ":25"
|
||||
s.Domain = config.Instance.Domain
|
||||
s.ReadTimeout = 10 * time.Second
|
||||
s.WriteTimeout = 10 * time.Second
|
||||
s.MaxMessageBytes = 1024 * 1024
|
||||
s.MaxRecipients = 50
|
||||
instance.Addr = ":25"
|
||||
instance.Domain = config.Instance.Domain
|
||||
instance.ReadTimeout = 10 * time.Second
|
||||
instance.WriteTimeout = 10 * time.Second
|
||||
instance.MaxMessageBytes = 1024 * 1024
|
||||
instance.MaxRecipients = 50
|
||||
// force TLS for auth
|
||||
s.AllowInsecureAuth = false
|
||||
instance.AllowInsecureAuth = false
|
||||
// Load the certificate and key
|
||||
cer, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
|
||||
if err != nil {
|
||||
@ -63,10 +65,16 @@ func Start() {
|
||||
return
|
||||
}
|
||||
// Configure the TLS support
|
||||
s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
|
||||
instance.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
|
||||
|
||||
log.Println("Starting server at", s.Addr)
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
log.Println("Starting server at", instance.Addr)
|
||||
if err := instance.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
if instance != nil {
|
||||
instance.Close()
|
||||
}
|
||||
}
|
||||
|
18
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))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user