diff --git a/Makefile b/Makefile
index 82c32f3..f5acc3a 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,7 @@ clean:
build_fe:
cd fe && yarn && yarn build
+ rm -rf server/http_server/dist
cd server && cp -rf ../fe/dist http_server
build_server:
diff --git a/fe/src/i18n/i18n.js b/fe/src/i18n/i18n.js
index d973653..acdc58a 100644
--- a/fe/src/i18n/i18n.js
+++ b/fe/src/i18n/i18n.js
@@ -63,7 +63,8 @@ var lang = {
"web_domain": "Web Domain",
"dns_desc": "Please add the following information to your DNS records",
"ssl_auto": "Automatically configure SSL certificates (recommended)",
- "wait_desc": "HTTP challenge mode completes in approximately 1 minute.",
+ "wait_desc": "Please Wait.",
+ "dns_challenge_wait": "DNS propagation and cache refreshes take a long time, and a wait of 10-30 minutes is possible here.",
"ssl_challenge_type":"Challenge Type",
"ssl_auto_http":"Http Request",
"ssl_auto_dns":"DNS Records",
@@ -176,7 +177,8 @@ var zhCN = {
"ssl_challenge_type":"验证方式",
"ssl_manuallyf": "手动配置SSL证书",
"challenge_typ_desc": "如果PMail直接使用80端口,建议使用HTTP验证方式。",
- "wait_desc": "HTTP验证模式大约1分钟完成",
+ "wait_desc": "请稍等",
+ "dns_challenge_wait": "DNS传播和缓存刷新时间较长,此处可能等待10-30分钟",
"ssl_key_path": "ssl key文件位置",
"ssl_crt_path": "ssl crt文件位置",
"group_settings": "分组",
diff --git a/fe/src/views/SetupView.vue b/fe/src/views/SetupView.vue
index f43831d..c75e1fc 100644
--- a/fe/src/views/SetupView.vue
+++ b/fe/src/views/SetupView.vue
@@ -133,16 +133,17 @@
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -313,7 +170,30 @@
- {{
+
+
+
+
+
+
+
+
+
+ {{ scope.row.value }}
+
+ {{ scope.row.value }}
+
+
+
+
+
+
+
+
+
+
+ {{
lang.next }}
@@ -324,10 +204,11 @@ import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import lang from '../i18n/i18n';
import axios from 'axios'
-
import { getCurrentInstance } from 'vue'
const app = getCurrentInstance()
const $http = app.appContext.config.globalProperties.$http
+const waitDesc = ref(lang.wait_desc)
+
const adminSettings = reactive({
"account": "admin",
@@ -349,18 +230,16 @@ const domainSettings = reactive({
const sslSettings = reactive({
"type": "0",
- "provider": "",
"challenge": "http",
"key_path": "./config/ssl/private.key",
"crt_path": "./config/ssl/public.crt",
- "paramsList": {},
+ "paramsList": [],
})
-const dnsApiParams = reactive({})
const active = ref(0)
const fullscreenLoading = ref(false)
-
+const dnsChecking = ref(false)
const dnsInfos = ref([
])
@@ -455,9 +334,9 @@ const getSSLConfig = () => {
ElMessage.error(res.errorMsg)
} else {
sslSettings.type = res.data.type
- if (sslSettings.type == "2"){
+ if (sslSettings.type == "2") {
sslSettings.type = "0"
- sslSettings.challenge="dns"
+ sslSettings.challenge = "dns"
}
@@ -475,24 +354,6 @@ const setSSLConfig = () => {
sslType = "2"
}
- if (sslType == "2") {
-
- let params = { "action": "setParams", "step": "ssl", };
-
- params = Object.assign(params, dnsApiParams);
-
- // dns验证方式先提交DNS api Key
- $http.post("/api/setup", params).then((res) => {
- if (res.errorNo != 0) {
- fullscreenLoading.value = false;
- ElMessage.error(res.errorMsg);
- return;
- }
- })
-
-
- }
-
$http.post("/api/setup", {
@@ -500,15 +361,18 @@ const setSSLConfig = () => {
"step": "ssl",
"ssl_type": sslType,
"key_path": sslSettings.key_path,
- "crt_path": sslSettings.crt_path,
- "serviceName": sslSettings.provider
+ "crt_path": sslSettings.crt_path
}).then((res) => {
if (res.errorNo != 0) {
fullscreenLoading.value = false;
ElMessage.error(res.errorMsg)
} else {
+ if (sslType == 2) {
+ fullscreenLoading.value = false;
+ dnsChecking.value = true;
+ getSSLDNSParams();
+ }
checkStatus();
-
}
})
}
@@ -542,16 +406,23 @@ const setDomainConfig = () => {
})
}
-const provide_change = () => {
- console.log(sslSettings.provider)
- $http.post("/api/setup", { "action": "getParams", "step": "ssl", "serverName": sslSettings.provider }).then((res) => {
+const getSSLDNSParams = () => {
+ $http.post("/api/setup", { "action": "getParams", "step": "ssl" }).then((res) => {
if (res.errorNo != 0) {
ElMessage.error(res.errorMsg)
} else {
sslSettings.paramsList = res.data
+ console.log(sslSettings.paramsList)
}
})
+ if (sslSettings.paramsList.length == 0) {
+ setTimeout(function () {
+ getSSLDNSParams()
+ }, 1000);
+ }
+
+
}
@@ -576,8 +447,12 @@ const next = () => {
active.value++
break
case 5:
- setSSLConfig();
- active.value++
+ if (dnsChecking.value) {
+ fullscreenLoading.value = true;
+ waitDesc.value = lang.dns_challenge_wait;
+ } else {
+ setSSLConfig();
+ }
break
}
diff --git a/server/config/config.go b/server/config/config.go
index 3423073..df4ccec 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -1,7 +1,12 @@
package config
import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
"encoding/json"
+ "encoding/pem"
"os"
)
@@ -48,8 +53,8 @@ func (c *Config) SetSetupPort(setupPort int) {
const DBTypeMySQL = "mysql"
const DBTypeSQLite = "sqlite"
const SSLTypeAutoHTTP = "0" //自动生成证书
-// const SSLTypeAutoDNS = "2" //自动生成证书,DNS api验证
-const SSLTypeUser = "1" //用户上传证书
+const SSLTypeAutoDNS = "2" //自动生成证书,DNS api验证
+const SSLTypeUser = "1" //用户上传证书
var DBTypes []string = []string{DBTypeMySQL, DBTypeSQLite}
@@ -86,3 +91,33 @@ func Init() {
}
}
+
+func ReadPrivateKey() (*ecdsa.PrivateKey, bool) {
+ key, err := os.ReadFile("./config/ssl/account_private.pem")
+ if err != nil {
+ return createNewPrivateKey(), true
+ }
+
+ block, _ := pem.Decode(key)
+ x509Encoded := block.Bytes
+ privateKey, _ := x509.ParseECPrivateKey(x509Encoded)
+
+ return privateKey, false
+}
+
+func createNewPrivateKey() *ecdsa.PrivateKey {
+ // Create a user. New accounts need an email and private key to start.
+ privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+ x509Encoded, _ := x509.MarshalECPrivateKey(privateKey)
+
+ // 将ec 密钥写入到 pem文件里
+ keypem, _ := os.OpenFile("./config/ssl/account_private.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+ err = pem.Encode(keypem, &pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
+ if err != nil {
+ panic(err)
+ }
+ return privateKey
+}
diff --git a/server/controllers/setup.go b/server/controllers/setup.go
index daa4075..b1aadcd 100644
--- a/server/controllers/setup.go
+++ b/server/controllers/setup.go
@@ -5,6 +5,7 @@ import (
log "github.com/sirupsen/logrus"
"io"
"net/http"
+ "os"
"pmail/config"
"pmail/dto/response"
"pmail/services/setup"
@@ -134,40 +135,36 @@ func Setup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
return
}
- //if reqData["step"] == "ssl" && reqData["action"] == "getParams" {
- // params, err := ssl.GetServerParamsList(reqData["serverName"])
- // if err != nil {
- // response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
- // return
- // }
- // response.NewSuccessResponse(params).FPrint(w)
- // return
- //}
-
- //if reqData["step"] == "ssl" && reqData["action"] == "setParams" {
- // for key, v := range reqData {
- // if key != "step" && key != "action" {
- // ssl.SetDomainServerParams(key, v)
- // }
- // }
- // response.NewSuccessResponse("Succ").FPrint(w)
- // return
- //}
+ if reqData["step"] == "ssl" && reqData["action"] == "getParams" {
+ dnsChallenge := ssl.GetDnsChallengeInstance()
+
+ response.NewSuccessResponse(dnsChallenge.GetDNSSettings(ctx)).FPrint(w)
+ return
+ }
if reqData["step"] == "ssl" && reqData["action"] == "set" {
+ keyPath := reqData["key_path"]
+ crtPath := reqData["crt_path"]
+
+ _, err := os.Stat(keyPath)
+ if err != nil {
+ response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+ return
+ }
- serviceName, ok := reqData["serviceName"]
- if !ok {
- serviceName = ""
+ _, err = os.Stat(crtPath)
+ if err != nil {
+ response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+ return
}
- err := ssl.SetSSL(reqData["ssl_type"], reqData["key_path"], reqData["crt_path"], serviceName)
+
+ err = ssl.SetSSL(reqData["ssl_type"], reqData["key_path"], reqData["crt_path"])
if err != nil {
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
return
}
- //if reqData["ssl_type"] == config.SSLTypeAutoHTTP || reqData["ssl_type"] == config.SSLTypeAutoDNS {
- if reqData["ssl_type"] == config.SSLTypeAutoHTTP {
+ if reqData["ssl_type"] == config.SSLTypeAutoHTTP || reqData["ssl_type"] == config.SSLTypeAutoDNS {
err = ssl.GenSSL(false)
if err != nil {
response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
diff --git a/server/cron_server/ssl_update.go b/server/cron_server/ssl_update.go
index 03a0359..a343219 100644
--- a/server/cron_server/ssl_update.go
+++ b/server/cron_server/ssl_update.go
@@ -22,7 +22,7 @@ func Start() {
}
}
- if config.Instance.SSLType == "0" {
+ if config.Instance.SSLType == config.SSLTypeAutoHTTP || config.Instance.SSLType == config.SSLTypeAutoDNS {
go sslUpdateLoop()
} else {
go sslCheck()
@@ -45,6 +45,7 @@ func sslCheck() {
log.Errorf("SSL Check Error! %+v", err)
}
if newExpTime != expiredTime {
+ expiredTime = newExpTime
log.Infoln("SSL certificate had update! restarting")
signal.RestartChan <- true
}
diff --git a/server/main_test.go b/server/main_test.go
index aa31f94..9a43e3f 100644
--- a/server/main_test.go
+++ b/server/main_test.go
@@ -72,6 +72,7 @@ func TestMaster(t *testing.T) {
t.Run("testSendEmail", testSendEmail)
time.Sleep(8 * time.Second)
t.Run("testEmailList", testEmailList)
+ t.Run("testGetDetail", testGetEmailDetail)
t.Run("testDelEmail", testDelEmail)
t.Run("testSendEmail2User1", testSendEmail2User1)
@@ -108,6 +109,23 @@ func testCheckRule(t *testing.T) {
}
}
+func testGetEmailDetail(t *testing.T) {
+ ret, err := httpClient.Post(TestHost+"/api/email/detail", "application/json", strings.NewReader(`{
+ "id":1
+}`))
+ if err != nil {
+ t.Error(err)
+ }
+ data, err := readResponse(ret.Body)
+ if err != nil {
+ t.Error(err)
+ }
+ if data.ErrorNo != 0 {
+ t.Error("GetEmailDetail Error! ", data)
+ }
+
+}
+
func testCreateRule(t *testing.T) {
ret, err := httpClient.Post(TestHost+"/api/rule/add", "application/json", strings.NewReader(`{
"name":"Move Group",
@@ -703,6 +721,7 @@ func testSendEmail2User3(t *testing.T) {
t.Logf("testSendEmail2User3 Success! Response: %+v", data)
}
+
func testEmailList(t *testing.T) {
ret, err := httpClient.Post(TestHost+"/api/email/list", "application/json", strings.NewReader(`{}`))
if err != nil {
diff --git a/server/services/detail/detail.go b/server/services/detail/detail.go
index 67cd8b5..f3098ea 100644
--- a/server/services/detail/detail.go
+++ b/server/services/detail/detail.go
@@ -27,7 +27,7 @@ func GetEmailDetail(ctx *context.Context, id int, markRead bool) (*response.Emai
//获取邮件内容
var email response.EmailResponseData
- _, err = db.Instance.ID(id).Get(&email)
+ _, err = db.Instance.Select("*,1 as is_read").Table("email").Where("id=?", id).Get(&email)
if err != nil {
log.WithContext(ctx).Errorf("SQL error:%+v", err)
return nil, err
diff --git a/server/services/setup/ssl/challenge.go b/server/services/setup/ssl/challenge.go
index f7aefac..5786619 100644
--- a/server/services/setup/ssl/challenge.go
+++ b/server/services/setup/ssl/challenge.go
@@ -1,5 +1,12 @@
package ssl
+import (
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ log "github.com/sirupsen/logrus"
+ "pmail/utils/context"
+ "time"
+)
+
type authInfo struct {
Domain string
Token string
@@ -35,3 +42,61 @@ func GetHttpChallengeInstance() *HttpChallenge {
}
return instance
}
+
+type DNSChallenge struct {
+ AuthInfo map[string]*authInfo
+}
+
+var dnsInstance *DNSChallenge
+
+func GetDnsChallengeInstance() *DNSChallenge {
+ if dnsInstance == nil {
+ dnsInstance = &DNSChallenge{
+ AuthInfo: map[string]*authInfo{},
+ }
+ }
+ return dnsInstance
+}
+
+func (h *DNSChallenge) Present(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+ log.Infof("Presenting challenge Info : %+v", info)
+ h.AuthInfo[token] = &authInfo{
+ Domain: info.FQDN,
+ Token: token,
+ KeyAuth: info.Value,
+ }
+ log.Infof("SSL Log:%s %s %s", domain, token, keyAuth)
+ return nil
+}
+
+func (h *DNSChallenge) CleanUp(domain, token, keyAuth string) error {
+ delete(h.AuthInfo, token)
+ return nil
+}
+
+func (h *DNSChallenge) Timeout() (timeout, interval time.Duration) {
+ return 60 * time.Minute, 5 * time.Second
+}
+
+type DNSItem struct {
+ Type string `json:"type"`
+ Host string `json:"host"`
+ Value string `json:"value"`
+ TTL int `json:"ttl"`
+ Tips string `json:"tips"`
+}
+
+func (h *DNSChallenge) GetDNSSettings(ctx *context.Context) []*DNSItem {
+ ret := []*DNSItem{}
+ for _, info := range h.AuthInfo {
+ ret = append(ret, &DNSItem{
+ Type: "TXT",
+ Host: info.Domain,
+ Value: info.KeyAuth,
+ TTL: 3600,
+ })
+ }
+
+ return ret
+}
diff --git a/server/services/setup/ssl/ssl.go b/server/services/setup/ssl/ssl.go
index fa6c825..b6b7d3e 100644
--- a/server/services/setup/ssl/ssl.go
+++ b/server/services/setup/ssl/ssl.go
@@ -3,11 +3,10 @@ package ssl
import (
"crypto"
"crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
"crypto/tls"
"crypto/x509"
"github.com/go-acme/lego/v4/certificate"
+ "github.com/go-acme/lego/v4/challenge/dns01"
log "github.com/sirupsen/logrus"
"github.com/spf13/cast"
"os"
@@ -51,15 +50,13 @@ func GetSSL() string {
return cfg.SSLType
}
-func SetSSL(sslType, priKey, crtKey, serviceName string) error {
+func SetSSL(sslType, priKey, crtKey string) error {
cfg, err := setup.ReadConfig()
if err != nil {
panic(err)
}
- //if sslType == config.SSLTypeAutoHTTP || sslType == config.SSLTypeUser || sslType == config.SSLTypeAutoDNS {
- if sslType == config.SSLTypeAutoHTTP || sslType == config.SSLTypeUser {
+ if sslType == config.SSLTypeAutoHTTP || sslType == config.SSLTypeUser || sslType == config.SSLTypeAutoDNS {
cfg.SSLType = sslType
- //cfg.DomainServiceName = serviceName
} else {
return errors.New("SSL Type Error!")
}
@@ -67,6 +64,8 @@ func SetSSL(sslType, priKey, crtKey, serviceName string) error {
if cfg.SSLType == config.SSLTypeUser {
cfg.SSLPrivateKeyPath = priKey
cfg.SSLPublicKeyPath = crtKey
+ // 手动设置证书的情况下后台地址默认不启用https
+ cfg.HttpsEnabled = 2
}
err = setup.WriteConfig(cfg)
@@ -77,28 +76,62 @@ func SetSSL(sslType, priKey, crtKey, serviceName string) error {
return nil
}
-func GenSSL(update bool) error {
+func renewCertificate(privateKey *ecdsa.PrivateKey, cfg *config.Config) error {
- cfg, err := setup.ReadConfig()
+ myUser := MyUser{
+ Email: "i@" + cfg.Domain,
+ key: privateKey,
+ }
+
+ conf := lego.NewConfig(&myUser)
+ conf.UserAgent = "PMail"
+ conf.Certificate.KeyType = certcrypto.RSA2048
+
+ // A client facilitates communication with the CA server.
+ client, err := lego.NewClient(conf)
+ if err != nil {
+ return errors.Wrap(err)
+ }
+
+ var reg *registration.Resource
+
+ reg, err = client.Registration.ResolveAccountByKey()
+ if err != nil {
+ return errors.Wrap(err)
+ }
+
+ myUser.Registration = reg
+
+ request := certificate.ObtainRequest{
+ Domains: []string{"smtp." + cfg.Domain, cfg.WebDomain, "pop." + cfg.Domain},
+ Bundle: true,
+ }
+
+ log.Infof("wait ssl renew")
+ certificates, err := client.Certificate.Obtain(request)
+ if err != nil {
+ panic(err)
+ }
+ err = os.WriteFile("./config/ssl/private.key", certificates.PrivateKey, 0666)
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
- }
+ err = os.WriteFile("./config/ssl/public.crt", certificates.Certificate, 0666)
+ if err != nil {
+ panic(err)
}
- // Create a user. New accounts need an email and private key to start.
- privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ err = os.WriteFile("./config/ssl/issuerCert.crt", certificates.IssuerCertificate, 0666)
if err != nil {
- return errors.Wrap(err)
+ panic(err)
}
+ return nil
+}
+
+func generateCertificate(privateKey *ecdsa.PrivateKey, cfg *config.Config, newAccount bool) error {
+
myUser := MyUser{
Email: "i@" + cfg.Domain,
key: privateKey,
@@ -114,31 +147,32 @@ func GenSSL(update bool) error {
return errors.Wrap(err)
}
- if cfg.SSLType == "0" {
+ if cfg.SSLType == config.SSLTypeAutoHTTP {
err = client.Challenge.SetHTTP01Provider(GetHttpChallengeInstance())
if err != nil {
return errors.Wrap(err)
}
+ } else if cfg.SSLType == config.SSLTypeAutoDNS {
+ err = client.Challenge.SetDNS01Provider(GetDnsChallengeInstance(), dns01.AddDNSTimeout(60*time.Minute))
+ if err != nil {
+ return errors.Wrap(err)
+ }
}
- //else if cfg.SSLType == "2" {
- // err = os.Setenv(strings.ToUpper(cfg.DomainServiceName)+"_PROPAGATION_TIMEOUT", "900")
- // if err != nil {
- // log.Errorf("Set ENV Variable Error: %s", err.Error())
- // }
- // dnspodProvider, err := dns.NewDNSChallengeProviderByName(cfg.DomainServiceName)
- // if err != nil {
- // return errors.Wrap(err)
- // }
- // err = client.Challenge.SetDNS01Provider(dnspodProvider, dns01.AddDNSTimeout(15*time.Minute))
- // if err != nil {
- // return errors.Wrap(err)
- // }
- //}
-
- reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
- if err != nil {
- return errors.Wrap(err)
+
+ var reg *registration.Resource
+
+ if newAccount {
+ reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
+ if err != nil {
+ return errors.Wrap(err)
+ }
+ } else {
+ reg, err = client.Registration.ResolveAccountByKey()
+ if err != nil {
+ return errors.Wrap(err)
+ }
}
+
myUser.Registration = reg
request := certificate.ObtainRequest{
@@ -154,7 +188,7 @@ func GenSSL(update bool) error {
if err != nil {
panic(err)
}
-
+ log.Infof("证书校验通过!")
err = os.WriteFile("./config/ssl/private.key", certificates.PrivateKey, 0666)
if err != nil {
panic(err)
@@ -177,6 +211,31 @@ func GenSSL(update bool) error {
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
+ }
+ }
+
+ privateKey, newAccount := config.ReadPrivateKey()
+
+ if !update {
+ return generateCertificate(privateKey, cfg, newAccount)
+ }
+
+ return renewCertificate(privateKey, cfg)
+}
+
// CheckSSLCrtInfo 返回证书过期剩余天数
func CheckSSLCrtInfo() (int, time.Time, error) {
@@ -207,7 +266,7 @@ func CheckSSLCrtInfo() (int, time.Time, error) {
}
func Update(needRestart bool) {
- if config.Instance != nil && config.Instance.IsInit && config.Instance.SSLType == "0" {
+ if config.Instance != nil && config.Instance.IsInit && (config.Instance.SSLType == config.SSLTypeAutoHTTP || config.Instance.SSLType == config.SSLTypeAutoDNS) {
days, _, err := CheckSSLCrtInfo()
if days < 30 || err != nil {
if err != nil {