This commit is contained in:
jinnrry 2023-08-12 10:09:22 +08:00
parent 4a26f52fde
commit 318fea24ad
42 changed files with 890 additions and 375 deletions

View File

@ -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、Buildor 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 !

View File

@ -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协议程序将自动获取并更新Lets 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

0
build.sh Executable file → Normal file
View File

BIN
docs/cn.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 KiB

BIN
docs/en.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)
}

View File

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

View File

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

View File

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

View 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)
}
}

View File

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

View File

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

View 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)
}
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View 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
View File

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

View File

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

View 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))
}