This commit is contained in:
jinnrry
2023-08-06 09:33:51 +08:00
parent 8f7fbd4fee
commit c0f12558b5
54 changed files with 1273 additions and 132 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea .idea
.DS_Store .DS_Store
dist dist
output output
pmail.db

View File

@@ -37,7 +37,11 @@
## 1、生成DKIM 秘钥 ## 1、生成DKIM 秘钥
使用[go-msgauth](https://github.com/emersion/go-msgauth)项目的dkim-keygen工具生成公钥和私钥 ```
go install github.com/emersion/go-msgauth/cmd/dkim-keygen@latest
dkim-keygen
```
执行后将得到`dkim.priv`文件,公钥数据会直接输出
生成以后将密钥放到`config/dkim`目录中 生成以后将密钥放到`config/dkim`目录中

13
build.sh Normal file → Executable file
View File

@@ -4,17 +4,14 @@ cd fe && yarn && yarn build
# 编译后端代码 # 编译后端代码
cd ../server && cp -rf ../fe/dist http_server cd ../server && cp -rf ../fe/dist http_server
CGO_ENABLED=0 GOOS=linux GOARCH=amd64
go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_linux_amd64 main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_linux_amd64 main.go
go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_windows_amd64 main.go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_windows_amd64 main.go
go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_mac_amd64 main.go
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_mac_amd64 main.go
go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_mac_arm64 main.go
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_mac_arm64 main.go
# 整理输出文件 # 整理输出文件
cd .. cd ..

View File

@@ -2,11 +2,11 @@
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import HomeHeader from '@/components/HomeHeader.vue' import HomeHeader from '@/components/HomeHeader.vue'
import HomeAside from '@/components/HomeAside.vue'; import HomeAside from '@/components/HomeAside.vue';
import { watch,ref } from 'vue' import { watch, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
const route = useRoute() const route = useRoute()
const pageName = ref(route.name) const pageName = ref(route.name)
watch( watch(
() => route.fullPath, () => route.fullPath,
@@ -21,7 +21,7 @@ watch(
<div id="main"> <div id="main">
<HomeHeader /> <HomeHeader />
<div id="content"> <div id="content">
<div id="aside" v-if="pageName != 'login'"> <div id="aside" v-if="pageName != 'login' && pageName != 'setup'">
<HomeAside /> <HomeAside />
</div> </div>
<div id="body"> <div id="body">

View File

@@ -9,7 +9,7 @@ var $http = axios.create({
timeout: 6000, //设置超时 timeout: 6000, //设置超时
headers: { headers: {
'Content-Type': 'application/json;charset=UTF-8;', 'Content-Type': 'application/json;charset=UTF-8;',
'Lang':lang.lang 'Lang': lang.lang
} }
}) })
@@ -27,7 +27,7 @@ $http.interceptors.request.use((config) => {
//响应拦截器 //响应拦截器
$http.interceptors.response.use((response) => { $http.interceptors.response.use((response) => {
//响应成功 //响应成功
if(response.data.errorNo == 403){ if (response.data.errorNo == 403) {
router.replace({ router.replace({
path: '/login', path: '/login',
query: { query: {
@@ -35,6 +35,15 @@ $http.interceptors.response.use((response) => {
} }
}) })
} }
//响应成功
if (response.data.errorNo == 402) {
router.replace({
path: '/setup',
query: {
redirect: router.currentRoute.fullPath
}
})
}
return response.data; return response.data;
}, (error) => { }, (error) => {
//响应错误 //响应错误

View File

@@ -18,7 +18,7 @@ var lang = {
"cc_desc": "Cc's e-mail address", "cc_desc": "Cc's e-mail address",
"send": "send", "send": "send",
"add_att": "Add Attachment", "add_att": "Add Attachment",
"attachment":"Attachment", "attachment": "Attachment",
"err_sender_must": "Sender's email prefix is required!", "err_sender_must": "Sender's email prefix is required!",
"only_prefix": "Only the email prefix is required!", "only_prefix": "Only the email prefix is required!",
"err_email_format": "Incorrect e-mail address, please check the e-mail format!", "err_email_format": "Incorrect e-mail address, please check the e-mail format!",
@@ -31,8 +31,28 @@ var lang = {
"succ": "Success!", "succ": "Success!",
"err_pwd_diff": "The passwords entered twice do not match!", "err_pwd_diff": "The passwords entered twice do not match!",
"fail": "Fail!", "fail": "Fail!",
"settings":"Settings", "settings": "Settings",
"security":"Security" "security": "Security",
"SetDomail": "Set Domain",
"setDNS": "Set DNS",
"setSSL": "Set SSL",
"setDatabase": "Set Database",
"setOther": "Other",
"welcome": "Welcome",
"next": "Next",
"tks_pmail": "Thanks for using Pmail",
"guid_desc": "Next, you will be guided to perform initial configuration. If you have already configured, please use your configuration file to overwrite the config folder of the running directory. If you have not configured it, please follow this guide.",
"select_db": "select database",
"db_desc": "PMail currently supports MySQL and SQLite3 databases, you can choose according to your needs.",
"type": "Type",
"db_select_ph": "please select your database",
"mysql_dsn": "MySQL DSN",
"sqlite_db_path": "Data File Path",
"domain_desc": "Set your domain infomation.",
"smtp_domain": "SMTP Domain",
"web_domain": "Web Domain",
"dns_desc": "Please add the following information to your DNS records",
}; };
@@ -57,7 +77,7 @@ var zhCN = {
"cc_desc": "抄送人邮箱地址", "cc_desc": "抄送人邮箱地址",
"send": "发送", "send": "发送",
"add_att": "添加附件", "add_att": "添加附件",
"attachment":"附件", "attachment": "附件",
"err_sender_must": "发件人邮箱前缀必填", "err_sender_must": "发件人邮箱前缀必填",
"only_prefix": "只需要邮箱前缀", "only_prefix": "只需要邮箱前缀",
"err_email_format": "邮箱地址错误,请检查邮箱格式!", "err_email_format": "邮箱地址错误,请检查邮箱格式!",
@@ -70,8 +90,27 @@ var zhCN = {
"succ": "成功!", "succ": "成功!",
"err_pwd_diff": "两次输入的密码不一致!", "err_pwd_diff": "两次输入的密码不一致!",
"fail": "失败", "fail": "失败",
"settings":"设置", "settings": "设置",
"security":"安全" "security": "安全",
"SetDomail": "域名设置",
"setDNS": "DNS设置",
"setSSL": "SSL设置",
"setDatabase": "数据库设置",
"setOther": "其他设置",
"welcome": "欢迎",
"next": "下一步",
"tks_pmail": "感谢使用PMail",
"guid_desc": "接下来将会指引你进行初始化配置如果你已有配置请使用你的配置文件覆盖运行目录的config文件夹。如果你没有配置过请按照该指引操作。",
"select_db": "数据库选择",
"db_desc": "PMail目前支持MySQL和SQLite3两种数据库你可根据需要选择。",
"type": "类型",
"db_select_ph": "请选择你的数据库",
"mysql_dsn": "MySQL DSN",
"sqlite_db_path": "存储位置",
"domain_desc": "设置你的域名信息。",
"smtp_domain": "SMTP域名地址",
"web_domain": "Web域名地址",
"dns_desc": "请将以下信息添加到DNS记录中"
} }
switch (navigator.language) { switch (navigator.language) {

View File

@@ -3,6 +3,9 @@ import ListView from '../views/ListView.vue'
import EditerView from '../views/EditerView.vue' import EditerView from '../views/EditerView.vue'
import LoginView from '../views/LoginView.vue' import LoginView from '../views/LoginView.vue'
import EmailDetailView from '../views/EmailDetailView.vue' import EmailDetailView from '../views/EmailDetailView.vue'
import SetupView from '../views/SetupView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [ routes: [
@@ -26,6 +29,11 @@ const router = createRouter({
name: "login", name: "login",
component: LoginView component: LoginView
}, },
{
path: '/setup',
name: "setup",
component: SetupView
},
{ {
path: '/detail/:id', path: '/detail/:id',
name: "detail", name: "detail",

View File

@@ -76,6 +76,8 @@
<script setup> <script setup>
import $http from '../http/http';
import '@wangeditor/editor/dist/css/style.css' // 引入 css import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { onBeforeUnmount, ref, shallowRef, reactive, onMounted } from 'vue' import { onBeforeUnmount, ref, shallowRef, reactive, onMounted } from 'vue'
@@ -83,7 +85,6 @@ import { Close } from '@element-plus/icons-vue';
import lang from '../i18n/i18n'; import lang from '../i18n/i18n';
import { Editor, Toolbar } from '@wangeditor/editor-for-vue' import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { i18nChangeLanguage } from '@wangeditor/editor' import { i18nChangeLanguage } from '@wangeditor/editor'
import $http from '../http/http';
import router from "@/router"; //根路由对象 import router from "@/router"; //根路由对象
import useGroupStore from '../stores/group' import useGroupStore from '../stores/group'
const groupStore = useGroupStore() const groupStore = useGroupStore()

View File

@@ -35,8 +35,9 @@
</template> </template>
<script setup> <script setup>
import { RouterLink } from 'vue-router'
import $http from "../http/http"; import $http from "../http/http";
import { RouterLink } from 'vue-router'
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import router from "@/router"; //根路由对象 import router from "@/router"; //根路由对象

View File

@@ -57,8 +57,9 @@
<script setup> <script setup>
import { RouterLink } from 'vue-router'
import $http from "../http/http"; import $http from "../http/http";
import { RouterLink } from 'vue-router'
import { reactive, ref, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import router from "@/router"; //根路由对象 import router from "@/router"; //根路由对象

233
fe/src/views/SetupView.vue Normal file
View File

@@ -0,0 +1,233 @@
<template>
<div id="main">
<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.SetDomail" />
<el-step :title="lang.setDNS" />
<el-step :title="lang.setSSL" />
<el-step :title="lang.setOther" />
</el-steps>
<div v-if="active == 0" class="ctn">
<div class="desc">
<h2>{{ lang.tks_pmail }}</h2>
<div style="margin-top: 10px;">{{ lang.guid_desc }}</div>
</div>
</div>
<div v-if="active == 1" class="ctn">
<div class="desc">
<h2>{{ lang.select_db }}</h2>
<div style="margin-top: 10px;">{{ lang.db_desc }}</div>
</div>
<div class="form" style="width: 400px;">
<el-form label-width="120px">
<el-form-item :label="lang.type">
<el-select :placeholder="lang.db_select_ph" v-model="dbSettings.type">
<el-option label="MySQL" value="mysql" />
<el-option label="SQLite3" value="sqlite" />
</el-select>
</el-form-item>
<el-form-item :label="lang.mysql_dsn" v-if="dbSettings.type == 'mysql'">
<el-input :rows="2" type="textarea" v-model="dbSettings.dsn"
placeholder="root:12345@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local"></el-input>
</el-form-item>
<el-form-item :label="lang.sqlite_db_path" v-if="dbSettings.type == 'sqlite'">
<el-input v-model="dbSettings.dsn" placeholder="./pmail.db"></el-input>
</el-form-item>
</el-form>
</div>
</div>
<div v-if="active == 2" class="ctn">
<div class="desc">
<h2>{{ lang.SetDomail }}</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.smtp_domain">
<el-input placeholder="domaim.com" v-model="domainSettings.smtp_domain">
<template #prepend>smtp.</template>
</el-input>
</el-form-item>
<el-form-item :label="lang.web_domain">
<el-input placeholder="pmail.domain.com" v-model="domainSettings.web_domain"></el-input>
</el-form-item>
</el-form>
</div>
</div>
<div v-if="active == 3" class="ctn_s">
<div class="desc">
<h2>{{ lang.setDNS }}</h2>
<div style="margin-top: 10px;">{{ lang.dns_desc }}</div>
</div>
<div class="form" width="600px">
<el-table :data="dnsInfos" border style="width: 100%">
<el-table-column prop="host" label="HOSTNAME" width="110px" />
<el-table-column prop="type" label="TYPE" width="110px" />
<el-table-column prop="value" label="VALUE">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-tooltip :content="scope.row.tips" placement="top" v-if="scope.row.tips != ''">
{{ scope.row.value }}
</el-tooltip>
<span v-else>{{ scope.row.value }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="ttl" label="TTL" width="110px" />
</el-table>
</div>
</div>
<el-button id="next" style="margin-top: 12px" @click="next">{{ lang.next }}</el-button>
</div>
</template>
<script setup>
import $http from "../http/http";
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import router from "@/router"; //根路由对象
import lang from '../i18n/i18n';
const dbSettings = reactive({
"type": "",
"dsn": "",
"lable": ""
})
const domainSettings = reactive({
"web_domain": "",
"smtp_domain": ""
})
const active = ref(0)
const dnsInfos = ref([
{ "host": "smtp", "type": "A", "value": "YouServerIp", "prid": "NA", "ttl": "3600" }
])
const getDbConfig = () => {
$http.post("/api/setup", { "action": "get", "step": "database" }).then((res) => {
if (res.errorNo != 0) {
ElMessage.error(res.errorMsg)
} else {
dbSettings.type = res.data.db_type;
dbSettings.dsn = res.data.db_dsn;
}
})
}
const getDomainConfig = () => {
$http.post("/api/setup", { "action": "get", "step": "domain" }).then((res) => {
if (res.errorNo != 0) {
ElMessage.error(res.errorMsg)
} else {
domainSettings.web_domain = res.data.web_domain;
domainSettings.smtp_domain = res.data.smtp_domain;
}
})
}
const setDbConfig = () => {
$http.post("/api/setup", { "action": "set", "step": "database", "db_type": dbSettings.type, "db_dsn": dbSettings.dsn }).then((res) => {
if (res.errorNo != 0) {
ElMessage.error(res.errorMsg)
} else {
active.value++;
getDomainConfig();
}
})
}
const getDNSConfig = () => {
$http.post("/api/setup", { "action": "get", "step": "dns" }).then((res) => {
if (res.errorNo != 0) {
ElMessage.error(res.errorMsg)
} else {
dnsInfos.value = res.data
}
})
}
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) {
ElMessage.error(res.errorMsg)
} else {
active.value++;
getDNSConfig();
}
})
}
const next = () => {
switch (active.value) {
case 0:
active.value++
getDbConfig();
break
case 1:
setDbConfig();
break;
case 2:
setDomainConfig();
break;
case 3:
active.value++
break
}
}
</script>
<style scoped>
#main {
width: 100%;
height: 100%;
background-color: #f1f1f1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.desc {
padding-right: 20px;
}
#status {}
.ctn {
display: flex;
justify-content: center;
}
.ctn_s {
display: flex;
flex-direction: column;
}
#next {}
</style>

View File

@@ -1,11 +1,14 @@
{ {
"domain": "jinnrry.com", "domain": "",
"webDomain": "",
"dkimPrivateKeyPath": "config/dkim/dkim.priv", "dkimPrivateKeyPath": "config/dkim/dkim.priv",
"SSLPrivateKeyPath": "config/ssl/private.key", "SSLPrivateKeyPath": "config/ssl/private.key",
"SSLPublicKeyPath": "config/ssl/public.crt", "SSLPublicKeyPath": "config/ssl/public.crt",
"mysqlDSN": "", "dbDSN": "",
"dbType": "",
"weChatPushAppId": "", "weChatPushAppId": "",
"weChatPushSecret": "", "weChatPushSecret": "",
"weChatPushTemplateId": "", "weChatPushTemplateId": "",
"weChatPushUserId": "" "weChatPushUserId": "",
"isInit": false
} }

View File

@@ -8,25 +8,35 @@ import (
"strings" "strings"
) )
var IsInit bool
type Config struct { type Config struct {
Domain string `json:"domain"` Domain string `json:"domain"`
DkimPrivateKeyPath string `json:"dkimPrivateKeyPath"` WebDomain string `json:"webDomain"`
SSLPrivateKeyPath string `json:"SSLPrivateKeyPath"` DkimPrivateKeyPath string `json:"dkimPrivateKeyPath"`
SSLPublicKeyPath string `json:"SSLPublicKeyPath"` SSLPrivateKeyPath string `json:"SSLPrivateKeyPath"`
MysqlDSN string `json:"mysqlDSN"` SSLPublicKeyPath string `json:"SSLPublicKeyPath"`
DbDSN string `json:"dbDSN"`
WeChatPushAppId string `json:"weChatPushAppId"` DbType string `json:"dbType"`
WeChatPushSecret string `json:"weChatPushSecret"` WeChatPushAppId string `json:"weChatPushAppId"`
WeChatPushTemplateId string `json:"weChatPushTemplateId"` WeChatPushSecret string `json:"weChatPushSecret"`
WeChatPushUserId string `json:"weChatPushUserId"` WeChatPushTemplateId string `json:"weChatPushTemplateId"`
WeChatPushUserId string `json:"weChatPushUserId"`
Tables map[string]string IsInit bool `json:"isInit"`
TablesInitData map[string]string Tables map[string]string `json:"-"`
TablesInitData map[string]string `json:"-"`
} }
//go:embed tables/* //go:embed tables/*
var tableConfig embed.FS var tableConfig embed.FS
const Version = "1.1.0"
const DBTypeMySQL = "mysql"
const DBTypeSQLite = "sqlite"
var DBTypes []string = []string{DBTypeMySQL, DBTypeSQLite}
var Instance *Config var Instance *Config
func Init() { func Init() {
@@ -37,25 +47,29 @@ func Init() {
if len(args) >= 2 && args[len(args)-1] == "dev" { if len(args) >= 2 && args[len(args)-1] == "dev" {
cfgData, err = os.ReadFile("./config/config.dev.json") cfgData, err = os.ReadFile("./config/config.dev.json")
if err != nil { if err != nil {
panic("dev环境配置文件加载失败" + err.Error()) return
} }
} else { } else {
cfgData, err = os.ReadFile("./config/config.json") cfgData, err = os.ReadFile("./config/config.json")
if err != nil { if err != nil {
panic("配置文件加载失败" + err.Error()) return
} }
} }
err = json.Unmarshal(cfgData, &Instance) err = json.Unmarshal(cfgData, &Instance)
if err != nil { if err != nil {
panic("配置文件加载失败" + err.Error()) return
} }
// 读取表设置 // 读取表设置
Instance.Tables = map[string]string{} Instance.Tables = map[string]string{}
Instance.TablesInitData = map[string]string{} Instance.TablesInitData = map[string]string{}
err = fs.WalkDir(tableConfig, "tables", func(path string, info fs.DirEntry, err error) error { root := "tables/mysql"
if Instance.DbType == DBTypeSQLite {
root = "tables/sqlite"
}
err = fs.WalkDir(tableConfig, root, func(path string, info fs.DirEntry, err error) error {
if !info.IsDir() && strings.HasSuffix(info.Name(), ".sql") { if !info.IsDir() && strings.HasSuffix(info.Name(), ".sql") {
tableName := strings.ReplaceAll(info.Name(), ".sql", "") tableName := strings.ReplaceAll(info.Name(), ".sql", "")
i, e := tableConfig.ReadFile(path) i, e := tableConfig.ReadFile(path)
@@ -76,4 +90,8 @@ func Init() {
panic(err) panic(err)
} }
if Instance.Domain != "" && Instance.IsInit {
IsInit = true
}
} }

View File

@@ -1,11 +1,14 @@
{ {
"domain": "demo.com", "domain": "",
"webDomain": "",
"dkimPrivateKeyPath": "config/dkim/dkim.priv", "dkimPrivateKeyPath": "config/dkim/dkim.priv",
"SSLPrivateKeyPath": "config/ssl/private.key", "SSLPrivateKeyPath": "config/ssl/private.key",
"SSLPublicKeyPath": "config/ssl/public.crt", "SSLPublicKeyPath": "config/ssl/public.crt",
"mysqlDSN": "", "dbDSN": "",
"dbType": "",
"weChatPushAppId": "", "weChatPushAppId": "",
"weChatPushSecret": "", "weChatPushSecret": "",
"weChatPushTemplateId": "", "weChatPushTemplateId": "",
"weChatPushUserId": "" "weChatPushUserId": "",
"isInit": false
} }

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
CREATE table email
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
type tinyint(4) NOT NULL DEFAULT 0 ,
subject varchar(1000) NOT NULL DEFAULT '' ,
reply_to json ,
from_name varchar(50) NOT NULL DEFAULT '' ,
from_address varchar(150) NOT NULL DEFAULT '' ,
`to` json ,
bcc json ,
cc json ,
`text` text ,
html text ,
sender json ,
attachments json ,
spf_check tinyint(1) DEFAULT 0 ,
dkim_check tinyint(1) DEFAULT 0 ,
status tinyint(4) NOT NULL DEFAULT 0 ,
send_user_id int unsigned NOT NULL DEFAULT 0 ,
is_read tinyint(1) NOT NULL DEFAULT 0 ,
error text ,
cron_send_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ,
send_date datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ,
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ,
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
)

View File

@@ -0,0 +1,8 @@
CREATE TABLE sessions
(
token TEXT PRIMARY KEY,
data BLOB NOT NULL,
expiry REAL NOT NULL
);
CREATE INDEX sessions_expiry_idx ON sessions (expiry);

View File

@@ -0,0 +1,8 @@
CREATE TABLE user
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
account varchar(20),
name varchar(10),
password char(32)
);
CREATE UNIQUE INDEX udx_account on user (account);

View File

@@ -0,0 +1,9 @@
CREATE TABLE user_auth
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id int ,
email_account varchar(30)
);
CREATE UNIQUE INDEX udx_uid_ename on user_auth ( user_id, email_account);
CREATE UNIQUE INDEX udx_ename_uid on user_auth ( email_account,user_id );

View File

@@ -7,12 +7,12 @@ import (
"io" "io"
"net/http" "net/http"
"pmail/config" "pmail/config"
"pmail/db"
"pmail/dto" "pmail/dto"
"pmail/dto/parsemail" "pmail/dto/parsemail"
"pmail/dto/response" "pmail/dto/response"
"pmail/hooks" "pmail/hooks"
"pmail/i18n" "pmail/i18n"
"pmail/mysql"
"pmail/smtp_server" "pmail/smtp_server"
"pmail/utils/async" "pmail/utils/async"
"strings" "strings"
@@ -139,7 +139,7 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
// 邮件落库 // 邮件落库
sql := "INSERT INTO email (type,subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,send_user_id,error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" sql := "INSERT INTO email (type,subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,send_user_id,error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
sqlRes, sqlerr := mysql.Instance.Exec(mysql.WithContext(ctx, sql), sqlRes, sqlerr := db.Instance.Exec(db.WithContext(ctx, sql),
1, 1,
e.Subject, e.Subject,
json2string(e.ReplyTo), json2string(e.ReplyTo),
@@ -181,12 +181,12 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
if err != nil { if err != nil {
errMsg = err.Error() errMsg = err.Error()
_, err := mysql.Instance.Exec(mysql.WithContext(ctx, "update email set status =2 ,error=? where id = ? "), errMsg, emailId) _, err := db.Instance.Exec(db.WithContext(ctx, "update email set status =2 ,error=? where id = ? "), errMsg, emailId)
if err != nil { if err != nil {
log.WithContext(ctx).Errorf("sql Error :%+v", err) log.WithContext(ctx).Errorf("sql Error :%+v", err)
} }
} else { } else {
_, err := mysql.Instance.Exec(mysql.WithContext(ctx, "update email set status =1 where id = ? "), emailId) _, err := db.Instance.Exec(db.WithContext(ctx, "update email set status =1 where id = ? "), emailId)
if err != nil { if err != nil {
log.WithContext(ctx).Errorf("sql Error :%+v", err) log.WithContext(ctx).Errorf("sql Error :%+v", err)
} }

View File

@@ -8,11 +8,11 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"io" "io"
"net/http" "net/http"
"pmail/db"
"pmail/dto" "pmail/dto"
"pmail/dto/response" "pmail/dto/response"
"pmail/i18n" "pmail/i18n"
"pmail/models" "pmail/models"
"pmail/mysql"
"pmail/session" "pmail/session"
) )
@@ -27,18 +27,18 @@ func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
if err != nil { if err != nil {
log.Errorf("%+v", err) log.Errorf("%+v", err)
} }
var retData loginRequest var reqData loginRequest
err = json.Unmarshal(reqBytes, &retData) err = json.Unmarshal(reqBytes, &reqData)
if err != nil { if err != nil {
log.Errorf("%+v", err) log.Errorf("%+v", err)
} }
var user models.User var user models.User
encodePwd := md5Encode(md5Encode(retData.Password+"pmail") + "pmail2023") encodePwd := md5Encode(md5Encode(reqData.Password+"pmail") + "pmail2023")
err = mysql.Instance.Get(&user, mysql.WithContext(ctx, "select * from user where account =? and password =?"), err = db.Instance.Get(&user, db.WithContext(ctx, "select * from user where account =? and password =?"),
retData.Account, encodePwd) reqData.Account, encodePwd)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
log.Errorf("%+v", err) log.Errorf("%+v", err)
} }

View File

@@ -5,10 +5,10 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"io" "io"
"net/http" "net/http"
"pmail/db"
"pmail/dto" "pmail/dto"
"pmail/dto/response" "pmail/dto/response"
"pmail/i18n" "pmail/i18n"
"pmail/mysql"
) )
type modifyPasswordRequest struct { type modifyPasswordRequest struct {
@@ -29,7 +29,7 @@ func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request)
if retData.Password != "" { if retData.Password != "" {
encodePwd := md5Encode(md5Encode(retData.Password+"pmail") + "pmail2023") encodePwd := md5Encode(md5Encode(retData.Password+"pmail") + "pmail2023")
_, err := mysql.Instance.Exec(mysql.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID) _, err := db.Instance.Exec(db.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID)
if err != nil { if err != nil {
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w) response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
return return

View File

@@ -0,0 +1,92 @@
package controllers
import (
"encoding/json"
"io"
"net/http"
"pmail/dto"
"pmail/dto/response"
"pmail/services/setup"
)
func Proxy(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("proxy"))
}
func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
reqBytes, err := io.ReadAll(req.Body)
if err != nil {
response.NewSuccessResponse("").FPrint(w)
return
}
var reqData map[string]string
err = json.Unmarshal(reqBytes, &reqData)
if err != nil {
response.NewSuccessResponse("").FPrint(w)
return
}
if reqData["step"] == "database" && reqData["action"] == "get" {
dbType, dbDSN, err := setup.GetDatabaseSettings()
if err != nil {
response.NewErrorResponse(response.ServerError, err.Error(), "")
}
response.NewSuccessResponse(map[string]string{
"db_type": dbType,
"db_dsn": dbDSN,
}).FPrint(w)
return
}
if reqData["step"] == "database" && reqData["action"] == "set" {
err := setup.SetDatabaseSettings(reqData["db_type"], reqData["db_dsn"])
if err != nil {
response.NewErrorResponse(response.ServerError, err.Error(), "")
}
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.NewSuccessResponse(map[string]string{
"smtp_domain": smtpDomain,
"web_domain": webDomain,
}).FPrint(w)
return
}
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.NewSuccessResponse("Succ").FPrint(w)
return
}
if reqData["step"] == "dns" && reqData["action"] == "get" {
dnsInfos, err := setup.GetDNSSettings(ctx)
if err != nil {
response.NewErrorResponse(response.ServerError, err.Error(), "")
}
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)
return
}
}

View File

@@ -1,10 +1,11 @@
package mysql package db
import ( import (
"fmt" "fmt"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
_ "modernc.org/sqlite"
"pmail/config" "pmail/config"
"pmail/dto" "pmail/dto"
) )
@@ -12,15 +13,23 @@ import (
var Instance *sqlx.DB var Instance *sqlx.DB
func Init() { func Init() {
dsn := config.Instance.MysqlDSN dsn := config.Instance.DbDSN
var err error var err error
Instance, err = sqlx.Open("mysql", dsn)
switch config.Instance.DbType {
case "mysql":
Instance, err = sqlx.Open("mysql", dsn)
case "sqlite":
Instance, err = sqlx.Open("sqlite", dsn)
default:
return
}
if err != nil { if err != nil {
panic(err) panic(err)
} }
Instance.SetMaxOpenConns(100) Instance.SetMaxOpenConns(100)
Instance.SetMaxIdleConns(10) Instance.SetMaxIdleConns(10)
showMySQLCharacterSet() //showMySQLCharacterSet()
checkTable() checkTable()
} }
@@ -38,7 +47,13 @@ type tables struct {
func checkTable() { func checkTable() {
var res []*tables var res []*tables
err := Instance.Select(&res, "show tables")
var err error
if config.Instance.DbType == "sqlite" {
err = Instance.Select(&res, "select name as `Tables_in_pmail` from sqlite_master where type='table'")
} else {
err = Instance.Select(&res, "show tables")
}
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -6,6 +6,7 @@ import (
) )
const ( const (
NeedSetup = 402
NeedLogin = 403 NeedLogin = 403
ParamsError = 100 ParamsError = 100
ServerError = 500 ServerError = 500

View File

@@ -4,25 +4,47 @@ go 1.20
require ( require (
github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24 github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24
github.com/alexedwards/scs/sqlite3store v0.0.0-20230327161757-10d4299e3b24
github.com/alexedwards/scs/v2 v2.5.1 github.com/alexedwards/scs/v2 v2.5.1
github.com/emersion/go-message v0.16.0
github.com/emersion/go-msgauth v0.6.6 github.com/emersion/go-msgauth v0.6.6
github.com/emersion/go-smtp v0.16.0 github.com/emersion/go-smtp v0.16.0
github.com/go-acme/lego/v4 v4.13.3
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/mileusna/spf v0.9.5 github.com/mileusna/spf v0.9.5
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.5.1 github.com/spf13/cast v1.5.1
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 golang.org/x/crypto v0.10.0
golang.org/x/text v0.3.8 golang.org/x/text v0.10.0
modernc.org/sqlite v1.24.0
) )
replace github.com/alexedwards/scs/sqlite3store v0.0.0-20230327161757-10d4299e3b24 => github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379
require ( require (
github.com/emersion/go-message v0.16.0 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/miekg/dns v1.1.50 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect github.com/google/uuid v1.3.0 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect github.com/mattn/go-isatty v0.0.19 // indirect
golang.org/x/tools v0.1.12 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/tools v0.10.0 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
) )

View File

@@ -1,10 +1,16 @@
github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379 h1:i6LB/3lgkRDupe3owyNXtH8dtQrdaReCLeAZKrWcqAE=
github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24 h1:1jXpX7IE/zuf9FZQJpqZNepXqW8mq6NLzplHDCA43HY= github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24 h1:1jXpX7IE/zuf9FZQJpqZNepXqW8mq6NLzplHDCA43HY=
github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24/go.mod h1:ShejCOaSJCEjCWjc7YBrgy2xd0Kp+wiyBdzTNQrAGn4= github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24/go.mod h1:ShejCOaSJCEjCWjc7YBrgy2xd0Kp+wiyBdzTNQrAGn4=
github.com/alexedwards/scs/v2 v2.5.1 h1:EhAz3Kb3OSQzD8T+Ub23fKsiuvE0GzbF5Lgn0uTwM3Y= github.com/alexedwards/scs/v2 v2.5.1 h1:EhAz3Kb3OSQzD8T+Ub23fKsiuvE0GzbF5Lgn0uTwM3Y=
github.com/alexedwards/scs/v2 v2.5.1/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/alexedwards/scs/v2 v2.5.1/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4= github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
@@ -20,26 +26,43 @@ github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/go-acme/lego/v4 v4.13.3 h1:aZ1S9FXIkCWG3Uw/rZKSD+MOuO8ZB1t6p9VCg6jJiNY=
github.com/go-acme/lego/v4 v4.13.3/go.mod h1:c/iodVGMeBXG/+KiQczoNkySo3YLWTVa0kiyeVd/FHc=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mileusna/spf v0.9.5 h1:P6cmaIBwrhZaP9stXMzGOtxe+gIu65OVbZCmrAv9rgU= github.com/mileusna/spf v0.9.5 h1:P6cmaIBwrhZaP9stXMzGOtxe+gIu65OVbZCmrAv9rgU=
github.com/mileusna/spf v0.9.5/go.mod h1:o6IdTae6QptAbLgx/+ueXSTSpkG+f1cqLemQJSew8sI= github.com/mileusna/spf v0.9.5/go.mod h1:o6IdTae6QptAbLgx/+ueXSTSpkG+f1cqLemQJSew8sI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@@ -47,26 +70,29 @@ github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0=
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -76,24 +102,50 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=

View File

@@ -13,10 +13,12 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"pmail/config"
"pmail/controllers" "pmail/controllers"
"pmail/controllers/email" "pmail/controllers/email"
"pmail/dto" "pmail/dto"
"pmail/dto/response" "pmail/dto/response"
"pmail/i18n"
"pmail/session" "pmail/session"
"time" "time"
) )
@@ -28,6 +30,37 @@ var ip string
const HttpPort = 80 const HttpPort = 80
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)
}
}
func SetupStop() {
err := setupServer.Close()
if err != nil {
panic(err)
}
}
func Start() { func Start() {
log.Infof("Http Server Start at :%d", HttpPort) log.Infof("Http Server Start at :%d", HttpPort)
@@ -118,15 +151,20 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
} }
ctx.Lang = lang ctx.Lang = lang
user := cast.ToString(session.Instance.Get(ctx, "user")) if config.IsInit {
if user != "" { user := cast.ToString(session.Instance.Get(ctx, "user"))
_ = json.Unmarshal([]byte(user), &ctx.UserInfo) if user != "" {
} _ = json.Unmarshal([]byte(user), &ctx.UserInfo)
if ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
if r.URL.Path != "/api/ping" && r.URL.Path != "/api/login" {
response.NewErrorResponse(response.NeedLogin, "登陆已失效!", "").FPrint(w)
return
} }
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)
return
}
}
} else if r.URL.Path != "/api/setup" {
response.NewErrorResponse(response.NeedSetup, "", "").FPrint(w)
return
} }
h(ctx, w, r) h(ctx, w, r)
} }

View File

@@ -11,6 +11,8 @@ var (
"succ": "成功", "succ": "成功",
"send_fail": "发送失败", "send_fail": "发送失败",
"att_err": "附件解码错误", "att_err": "附件解码错误",
"login_exp": "登录已失效",
"ip_taps": "这是你服务器IP确保这个IP正确",
} }
en = map[string]string{ en = map[string]string{
"all_email": "All Email", "all_email": "All Email",
@@ -22,6 +24,8 @@ var (
"succ": "Success", "succ": "Success",
"send_fail": "Send Failure", "send_fail": "Send Failure",
"att_err": "Attachment decoding error", "att_err": "Attachment decoding error",
"login_exp": "Login has expired.",
"ip_taps": "This is your server's IP, make sure it is correct.",
} }
) )

View File

@@ -7,12 +7,7 @@ import (
"os" "os"
"pmail/config" "pmail/config"
"pmail/dto" "pmail/dto"
"pmail/dto/parsemail" "pmail/res_init"
"pmail/hooks"
"pmail/http_server"
"pmail/mysql"
"pmail/session"
"pmail/smtp_server"
"time" "time"
) )
@@ -57,20 +52,10 @@ func main() {
var cst, _ = time.LoadLocation("Asia/Shanghai") var cst, _ = time.LoadLocation("Asia/Shanghai")
time.Local = cst time.Local = cst
config.Init() res_init.Init()
parsemail.Init()
mysql.Init()
session.Init()
hooks.Init()
// smtp server start
go smtp_server.Start()
// http server start
go http_server.Start()
log.Infoln("***************************************************") log.Infoln("***************************************************")
log.Infoln("***\tServer Start Success Version:1.0.0") log.Infof("***\tServer Start Success Version:%s\n", config.Version)
log.Infof("***\tGit Commit Hash: %s ", gitHash) log.Infof("***\tGit Commit Hash: %s ", gitHash)
log.Infof("***\tBuild TimeStamp: %s ", buildTime) log.Infof("***\tBuild TimeStamp: %s ", buildTime)
log.Infof("***\tBuild GoLang Version: %s ", goVersion) log.Infof("***\tBuild GoLang Version: %s ", goVersion)

54
server/res_init/init.go Normal file
View File

@@ -0,0 +1,54 @@
package res_init
import (
"os"
"pmail/config"
"pmail/db"
"pmail/dto/parsemail"
"pmail/hooks"
"pmail/http_server"
"pmail/session"
"pmail/smtp_server"
"pmail/utils/file"
)
func Init() {
config.Init()
if config.IsInit {
parsemail.Init()
db.Init()
session.Init()
hooks.Init()
// smtp server start
go smtp_server.Start()
// http server start
go http_server.Start()
} else {
dirInit()
go http_server.SetupStart()
}
}
func dirInit() {
if !file.PathExist("./config") {
err := os.MkdirAll("./config", 0744)
if err != nil {
panic(err)
}
}
if !file.PathExist("./config/dkim") {
err := os.MkdirAll("./config/dkim", 0744)
if err != nil {
panic(err)
}
}
if !file.PathExist("./config/ssl") {
err := os.MkdirAll("./config/ssl", 0744)
if err != nil {
panic(err)
}
}
}

View File

@@ -3,10 +3,10 @@ package attachments
import ( import (
"encoding/json" "encoding/json"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"pmail/db"
"pmail/dto" "pmail/dto"
"pmail/dto/parsemail" "pmail/dto/parsemail"
"pmail/models" "pmail/models"
"pmail/mysql"
"pmail/services/auth" "pmail/services/auth"
) )
@@ -14,7 +14,7 @@ func GetAttachments(ctx *dto.Context, emailId int, cid string) (string, []byte)
// 获取邮件内容 // 获取邮件内容
var email models.Email var email models.Email
err := mysql.Instance.Get(&email, mysql.WithContext(ctx, "select * from email where id = ?"), emailId) err := db.Instance.Get(&email, db.WithContext(ctx, "select * from email where id = ?"), emailId)
if err != nil { if err != nil {
log.WithContext(ctx).Errorf("SQL error:%+v", err) log.WithContext(ctx).Errorf("SQL error:%+v", err)
return "", nil return "", nil
@@ -39,7 +39,7 @@ func GetAttachmentsByIndex(ctx *dto.Context, emailId int, index int) (string, []
// 获取邮件内容 // 获取邮件内容
var email models.Email var email models.Email
err := mysql.Instance.Get(&email, mysql.WithContext(ctx, "select * from email where id = ?"), emailId) err := db.Instance.Get(&email, db.WithContext(ctx, "select * from email where id = ?"), emailId)
if err != nil { if err != nil {
log.WithContext(ctx).Errorf("SQL error:%+v", err) log.WithContext(ctx).Errorf("SQL error:%+v", err)
return "", nil return "", nil

View File

@@ -1,10 +1,17 @@
package auth package auth
import ( import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"os"
"pmail/db"
"pmail/dto" "pmail/dto"
"pmail/models" "pmail/models"
"pmail/mysql"
"strings" "strings"
) )
@@ -12,7 +19,7 @@ import (
func HasAuth(ctx *dto.Context, email *models.Email) bool { func HasAuth(ctx *dto.Context, email *models.Email) bool {
// 获取当前用户的auth // 获取当前用户的auth
var auth []models.UserAuth var auth []models.UserAuth
err := mysql.Instance.Select(&auth, mysql.WithContext(ctx, "select * from user_auth where user_id = ?"), ctx.UserInfo.ID) err := db.Instance.Select(&auth, db.WithContext(ctx, "select * from user_auth where user_id = ?"), ctx.UserInfo.ID)
if err != nil { if err != nil {
log.WithContext(ctx).Errorf("SQL error:%+v", err) log.WithContext(ctx).Errorf("SQL error:%+v", err)
return false return false
@@ -31,3 +38,74 @@ func HasAuth(ctx *dto.Context, email *models.Email) bool {
return hasAuth return hasAuth
} }
func DkimGen() string {
privKeyStr, _ := os.ReadFile("./config/dkim/dkim.priv")
publicKeyStr, _ := os.ReadFile("./config/dkim/dkim.public")
if len(privKeyStr) > 0 && len(publicKeyStr) > 0 {
return string(publicKeyStr)
}
var (
privKey crypto.Signer
err error
)
privKey, err = rsa.GenerateKey(rand.Reader, 3072)
if err != nil {
log.Fatalf("Failed to generate key: %v", err)
}
privBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
if err != nil {
log.Fatalf("Failed to marshal private key: %v", err)
}
f, err := os.OpenFile("./config/dkim/dkim.priv", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to create key file: %v", err)
}
defer f.Close()
privBlock := pem.Block{
Type: "PRIVATE KEY",
Bytes: privBytes,
}
if err := pem.Encode(f, &privBlock); err != nil {
log.Fatalf("Failed to write key PEM block: %v", err)
}
if err := f.Close(); err != nil {
log.Fatalf("Failed to close key file: %v", err)
}
var pubBytes []byte
switch pubKey := privKey.Public().(type) {
case *rsa.PublicKey:
// RFC 6376 is inconsistent about whether RSA public keys should
// be formatted as RSAPublicKey or SubjectPublicKeyInfo.
// Erratum 3017 (https://www.rfc-editor.org/errata/eid3017)
// proposes allowing both. We use SubjectPublicKeyInfo for
// consistency with other implementations including opendkim,
// Gmail, and Fastmail.
pubBytes, err = x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
log.Fatalf("Failed to marshal public key: %v", err)
}
default:
panic("unreachable")
}
params := []string{
"v=DKIM1",
"k=rsa",
"p=" + base64.StdEncoding.EncodeToString(pubBytes),
}
publicKey := strings.Join(params, "; ")
os.WriteFile("./config/dkim/dkim.public", []byte(publicKey), 0666)
return publicKey
}

View File

@@ -5,24 +5,24 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"pmail/db"
"pmail/dto" "pmail/dto"
"pmail/dto/parsemail" "pmail/dto/parsemail"
"pmail/models" "pmail/models"
"pmail/mysql"
"strings" "strings"
) )
func GetEmailDetail(ctx *dto.Context, id int, markRead bool) (*models.Email, error) { func GetEmailDetail(ctx *dto.Context, id int, markRead bool) (*models.Email, error) {
// 获取邮件内容 // 获取邮件内容
var email models.Email var email models.Email
err := mysql.Instance.Get(&email, mysql.WithContext(ctx, "select * from email where id = ?"), id) err := db.Instance.Get(&email, db.WithContext(ctx, "select * from email where id = ?"), id)
if err != nil { if err != nil {
log.WithContext(ctx).Errorf("SQL error:%+v", err) log.WithContext(ctx).Errorf("SQL error:%+v", err)
return nil, err return nil, err
} }
if markRead && email.IsRead == 0 { if markRead && email.IsRead == 0 {
_, err = mysql.Instance.Exec(mysql.WithContext(ctx, "update email set is_read =1 where id =?"), email.Id) _, err = db.Instance.Exec(db.WithContext(ctx, "update email set is_read =1 where id =?"), email.Id)
if err != nil { if err != nil {
log.WithContext(ctx).Errorf("SQL error:%+v", err) log.WithContext(ctx).Errorf("SQL error:%+v", err)
} }

View File

@@ -3,9 +3,9 @@ package list
import ( import (
"encoding/json" "encoding/json"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"pmail/db"
"pmail/dto" "pmail/dto"
"pmail/models" "pmail/models"
"pmail/mysql"
) )
func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit int) (emailList []*models.Email, total int) { func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit int) (emailList []*models.Email, total int) {
@@ -13,12 +13,12 @@ func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit in
querySQL, queryParams := genSQL(ctx, false, tag, keyword, offset, limit) querySQL, queryParams := genSQL(ctx, false, tag, keyword, offset, limit)
counterSQL, counterParams := genSQL(ctx, true, tag, keyword, offset, limit) counterSQL, counterParams := genSQL(ctx, true, tag, keyword, offset, limit)
err := mysql.Instance.Select(&emailList, querySQL, queryParams...) err := db.Instance.Select(&emailList, querySQL, queryParams...)
if err != nil { if err != nil {
log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err) log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
} }
err = mysql.Instance.Get(&total, counterSQL, counterParams...) err = db.Instance.Get(&total, counterSQL, counterParams...)
if err != nil { if err != nil {
log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err) log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
} }

View File

@@ -0,0 +1,76 @@
package setup
import (
"encoding/json"
"os"
"pmail/config"
"pmail/utils/array"
"pmail/utils/errors"
"pmail/utils/file"
)
func GetDatabaseSettings() (string, string, error) {
configData, err := readConfig()
if err != nil {
return "", "", errors.Wrap(err)
}
return configData.DbType, configData.DbDSN, nil
}
func SetDatabaseSettings(dbType, dbDSN string) error {
configData, err := readConfig()
if err != nil {
return errors.Wrap(err)
}
if !array.InArray(dbType, config.DBTypes) {
return errors.New("dbtype error")
}
configData.DbType = dbType
configData.DbDSN = dbDSN
// 检查数据库是否能正确连接 todo
err = writeConfig(configData)
if err != nil {
return errors.Wrap(err)
}
return nil
}
func writeConfig(cfg *config.Config) error {
bytes, _ := json.Marshal(cfg)
err := os.WriteFile("./config/config.json", bytes, 0666)
if err != nil {
return errors.Wrap(err)
}
return nil
}
func readConfig() (*config.Config, error) {
configData := config.Config{
DkimPrivateKeyPath: "config/dkim/dkim.priv",
SSLPrivateKeyPath: "config/ssl/private.key",
SSLPublicKeyPath: "config/ssl/public.crt",
}
if !file.PathExist("./config/config.json") {
bytes, _ := json.Marshal(configData)
err := os.WriteFile("./config/config.json", bytes, 0666)
if err != nil {
return nil, errors.Wrap(err)
}
} else {
cfgData, err := os.ReadFile("./config/config.json")
if err != nil {
return nil, errors.Wrap(err)
}
err = json.Unmarshal(cfgData, &configData)
if err != nil {
return nil, errors.Wrap(err)
}
}
return &configData, nil
}

View File

@@ -0,0 +1,54 @@
package setup
import (
"encoding/json"
"fmt"
"io"
"net/http"
"pmail/dto"
"pmail/i18n"
"pmail/services/auth"
"pmail/utils/errors"
)
type DNSItem struct {
Type string `json:"type"`
Host string `json:"host"`
Value string `json:"value"`
TTL int `json:"ttl"`
Tips string `json:"tips"`
}
func GetDNSSettings(ctx *dto.Context) ([]*DNSItem, error) {
configData, err := readConfig()
if err != nil {
return nil, errors.Wrap(err)
}
ret := []*DNSItem{
{Type: "A", Host: "smtp", Value: getIp(), TTL: 3600, Tips: i18n.GetText(ctx.Lang, "ip_taps")},
{Type: "MX", Host: "-", Value: fmt.Sprintf("smtp.%s", configData.Domain), TTL: 3600},
{Type: "TXT", Host: "-", Value: "v=spf1 a mx ~all", TTL: 3600},
{Type: "TXT", Host: "default._domainkey", Value: auth.DkimGen(), TTL: 3600},
}
return ret, nil
}
func getIp() string {
resp, err := http.Get("http://ip-api.com/json/?lang=zh-CN ")
if err != nil {
return "Your Server IP"
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
body, err := io.ReadAll(resp.Body)
if err == nil {
var queryRes map[string]string
_ = json.Unmarshal(body, &queryRes)
return queryRes["query"]
}
}
return "Your Server IP"
}

View File

@@ -0,0 +1,7 @@
package setup
import "testing"
func TestGetIp(t *testing.T) {
GetIp()
}

View File

@@ -0,0 +1,32 @@
package setup
import (
"pmail/utils/errors"
)
func GetDomainSettings() (string, string, error) {
configData, err := readConfig()
if err != nil {
return "", "", errors.Wrap(err)
}
return configData.Domain, configData.WebDomain, nil
}
func SetDomainSettings(smtpDomain, webDomain string) error {
configData, err := readConfig()
if err != nil {
return errors.Wrap(err)
}
configData.Domain = smtpDomain
configData.WebDomain = webDomain
// 检查域名是否指向本机 todo
err = writeConfig(configData)
if err != nil {
return errors.Wrap(err)
}
return nil
}

View File

@@ -0,0 +1,103 @@
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

@@ -2,8 +2,10 @@ package session
import ( import (
"github.com/alexedwards/scs/mysqlstore" "github.com/alexedwards/scs/mysqlstore"
"github.com/alexedwards/scs/sqlite3store"
"github.com/alexedwards/scs/v2" "github.com/alexedwards/scs/v2"
"pmail/mysql" "pmail/config"
"pmail/db"
"time" "time"
) )
@@ -13,7 +15,11 @@ var Instance *scs.SessionManager
func Init() { func Init() {
Instance = scs.New() Instance = scs.New()
Instance.Lifetime = 24 * time.Hour Instance.Lifetime = 24 * time.Hour
// 使用mysql存储session数据目前为了架构简单 // 使用db存储session数据目前为了架构简单
// 暂不引入redis存储如果日后性能存在瓶颈可以将session迁移到redis // 暂不引入redis存储如果日后性能存在瓶颈可以将session迁移到redis
Instance.Store = mysqlstore.New(mysql.Instance.DB) if config.Instance.DbType == "mysql" {
Instance.Store = mysqlstore.New(db.Instance.DB)
} else {
Instance.Store = sqlite3store.New(db.Instance.DB)
}
} }

View File

@@ -8,9 +8,9 @@ import (
"io" "io"
"net" "net"
"net/netip" "net/netip"
"pmail/db"
"pmail/dto/parsemail" "pmail/dto/parsemail"
"pmail/hooks" "pmail/hooks"
"pmail/mysql"
"pmail/utils/async" "pmail/utils/async"
"strings" "strings"
"time" "time"
@@ -65,7 +65,7 @@ func (s *Session) Data(r io.Reader) error {
} }
sql := "INSERT INTO email (send_date, subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" sql := "INSERT INTO email (send_date, subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
_, err = mysql.Instance.Exec(sql, _, err = db.Instance.Exec(sql,
email.Date, email.Date,
email.Subject, email.Subject,
json2string(email.ReplyTo), json2string(email.ReplyTo),

View File

@@ -9,8 +9,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"pmail/config" "pmail/config"
"pmail/db"
parsemail2 "pmail/dto/parsemail" parsemail2 "pmail/dto/parsemail"
"pmail/mysql"
"pmail/session" "pmail/session"
"testing" "testing"
"time" "time"
@@ -39,7 +39,7 @@ func testInit() {
config.Init() config.Init()
parsemail2.Init() parsemail2.Init()
mysql.Init() db.Init()
session.Init() session.Init()
} }

View File

@@ -17,3 +17,78 @@ func Join[T any](arg []T, str string) string {
} }
return ret.String() return ret.String()
} }
// Unique 数组去重
func Unique[T comparable](slice []T) []T {
mp := map[T]bool{}
for _, v := range slice {
mp[v] = true
}
ret := []T{}
for t, _ := range mp {
ret = append(ret, t)
}
return ret
}
// Merge 求并集
func Merge[T any](slice1, slice2 []T) []T {
s1Len := len(slice1)
slice3 := make([]T, s1Len+len(slice2))
for i, t := range slice1 {
slice3[i] = t
}
for i, t := range slice2 {
slice3[s1Len+i] = t
}
return slice3
}
// Intersect 求交集
func Intersect[T comparable](slice1, slice2 []T) []T {
m := make(map[T]bool)
nn := make([]T, 0)
for _, v := range slice1 {
m[v] = true
}
for _, v := range slice2 {
exist, _ := m[v]
if exist {
nn = append(nn, v)
}
}
return nn
}
// Difference 求差集 slice1-并集
func Difference[T comparable](slice1, slice2 []T) []T {
m := make(map[T]bool)
nn := make([]T, 0)
inter := Intersect(slice1, slice2)
for _, v := range inter {
m[v] = true
}
for _, value := range slice1 {
exist, _ := m[value]
if !exist {
nn = append(nn, value)
}
}
return nn
}
// InArray 判断元素是否在数组中
func InArray[T comparable](needle T, haystack []T) bool {
for _, t := range haystack {
if needle == t {
return true
}
}
return false
}

View File

@@ -0,0 +1,34 @@
package errors
import (
oe "errors"
"fmt"
"runtime"
)
func New(text string) error {
_, file, line, _ := runtime.Caller(1)
return oe.New(fmt.Sprintf("%s at %s:%d", text, file, line))
}
func Wrap(err error) error {
_, file, line, _ := runtime.Caller(1)
return fmt.Errorf("at %s:%d\n%w", file, line, err)
}
func WrapWithMsg(err error, msg string) error {
_, file, line, _ := runtime.Caller(1)
return fmt.Errorf("%s at %s:%d\n%w", msg, file, line, err)
}
func Unwrap(err error) error {
return oe.Unwrap(err)
}
func Is(err, target error) bool {
return oe.Is(err, target)
}
func As(err error, target any) bool {
return oe.As(err, target)
}

View File

@@ -0,0 +1,29 @@
package errors
import (
"fmt"
"testing"
)
func TestNew(t *testing.T) {
err := New("err")
fmt.Println(err)
}
func TestWarp(t *testing.T) {
err := New("err1")
err = Wrap(err)
err = Wrap(err)
err = Wrap(err)
err = Wrap(err)
fmt.Println(err)
}
func TestWarpWithMsg(t *testing.T) {
err := New("err1")
err = Wrap(err)
err = Wrap(err)
err = Wrap(err)
err = WrapWithMsg(err, "last")
fmt.Println(err)
}

11
server/utils/file/file.go Normal file
View File

@@ -0,0 +1,11 @@
package file
import "os"
func PathExist(_path string) bool {
_, err := os.Stat(_path)
if err != nil && os.IsNotExist(err) {
return false
}
return true
}