mirror of
https://github.com/Jinnrry/PMail.git
synced 2025-02-20 11:43:09 +08:00
commit
a2e5c3afd6
36
README.md
36
README.md
@ -49,7 +49,11 @@ beautiful and cute Logo for this project!
|
||||
|
||||
## 2、Run
|
||||
|
||||
`double-click to open` Or `execute command to run`
|
||||
`./pmail`
|
||||
|
||||
Or
|
||||
|
||||
`docker run -p 25:25 -p 80:80 -p 443:443 -p 465:465 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
|
||||
|
||||
## 3、Configuration
|
||||
|
||||
@ -70,6 +74,36 @@ and restart the service.
|
||||
Create bot and get token from [BotFather](https://t.me/BotFather)
|
||||
Open the `config/config.json` file in the run directory, edit a few configuration items at the beginning of `tg`and restart the service.
|
||||
|
||||
# Configuration file format description
|
||||
|
||||
```json
|
||||
{
|
||||
"logLevel": "info", //log output level
|
||||
"domain": "domain.com", // Your domain
|
||||
"webDomain": "mail.domain.com", // web domain
|
||||
"dkimPrivateKeyPath": "config/dkim/dkim.priv", // dkim key path
|
||||
"sslType": "0", // ssl certificate update mode, 0 automatic, 1 manual
|
||||
"SSLPrivateKeyPath": "config/ssl/private.key", // ssl certificate path
|
||||
"SSLPublicKeyPath": "config/ssl/public.crt", // ssl certificate path
|
||||
"dbDSN": "./config/pmail.db", // database connect DSN
|
||||
"dbType": "sqlite", //database type ,`sqlite` or `mysql`
|
||||
"httpsEnabled": 0, // enabled https , 0:enabled 1:enablde 2:disenabled
|
||||
"httpPort": 80, // http port . default 80
|
||||
"httpsPort": 443, // https port . default 443
|
||||
"spamFilterLevel": 0,// Spam filter level, 0: no filter, 1: filtering when `spf` and `dkim` don't pass, 2: filtering when `spf` don't pass
|
||||
"weChatPushAppId": "", // wechat appid
|
||||
"weChatPushSecret": "", // weChat Secret
|
||||
"weChatPushTemplateId": "", // weChat TemplateId
|
||||
"weChatPushUserId": "", // weChat UserId
|
||||
"tgChatId": "", // telegram chatid
|
||||
"tgBotToken": "", // telegram token
|
||||
"isInit": true // If false, it will enter the bootstrap process.
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
# For Developer
|
||||
|
||||
## Project Framework
|
||||
|
34
README_CN.md
34
README_CN.md
@ -53,7 +53,11 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
|
||||
|
||||
## 2、运行
|
||||
|
||||
双击打开 OR 执行命令运行
|
||||
`./pmail`
|
||||
|
||||
或者
|
||||
|
||||
`docker run -p 25:25 -p 80:80 -p 443:443 -p 465:465 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
|
||||
|
||||
## 3、配置
|
||||
|
||||
@ -70,6 +74,34 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
|
||||
## 6、Telegram推送
|
||||
从 [BotFather](https://t.me/BotFather) 创建并获取令牌机器人。 打开运行目录下的 config/config.json 文件,编辑 `tg` 开头的几个配置项,重启服务即可。
|
||||
|
||||
|
||||
# 配置文件说明
|
||||
|
||||
```json
|
||||
{
|
||||
"logLevel": "info", //日志输出级别
|
||||
"domain": "domain.com", // 你的域名
|
||||
"webDomain": "mail.domain.com", // web域名
|
||||
"dkimPrivateKeyPath": "config/dkim/dkim.priv", // dkim 私钥地址
|
||||
"sslType": "0", // ssl证书更新模式,0自动,1手动
|
||||
"SSLPrivateKeyPath": "config/ssl/private.key", // ssl 证书地址
|
||||
"SSLPublicKeyPath": "config/ssl/public.crt", // ssl 证书地址
|
||||
"dbDSN": "./config/pmail.db", // 数据库连接DSN
|
||||
"dbType": "sqlite", //数据库类型,支持sqlite 和 mysql
|
||||
"httpsEnabled": 0, // web后台是否启用https 0默认(启用),1启用,2不启用
|
||||
"spamFilterLevel": 0,// 垃圾邮件过滤级别,0不过滤、1 spf dkim 校验均失败时过滤,2 spf校验不通过时过滤
|
||||
"httpPort": 80, // http 端口 . 默认 80
|
||||
"httpsPort": 443, // https 端口 . 默认 443
|
||||
"weChatPushAppId": "", // 微信推送appid
|
||||
"weChatPushSecret": "", // 微信推送秘钥
|
||||
"weChatPushTemplateId": "", // 微信推送模板id
|
||||
"weChatPushUserId": "", // 微信推送用户id
|
||||
"tgChatId": "", // telegram 推送chatid
|
||||
"tgBotToken": "", // telegram 推送 token
|
||||
"isInit": true // 为false的时候会进入安装引导流程
|
||||
}
|
||||
```
|
||||
|
||||
# 参与开发
|
||||
|
||||
## 项目架构
|
||||
|
2505
fe/package-lock.json
generated
2505
fe/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,13 +9,17 @@
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-drawer v-model="openSettings" size="80%" :title="lang.settings">
|
||||
<el-tabs tab-position="left" >
|
||||
<el-tabs tab-position="left">
|
||||
<el-tab-pane :label="lang.security">
|
||||
<SecuritySettings/>
|
||||
<SecuritySettings />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="lang.group_settings">
|
||||
<GroupSettings/>
|
||||
<GroupSettings />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="lang.rule_setting">
|
||||
<RuleSettings />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
@ -30,7 +34,7 @@ import { ElMessageBox } from 'element-plus'
|
||||
import SecuritySettings from '@/components/SecuritySettings.vue'
|
||||
import lang from '../i18n/i18n';
|
||||
import GroupSettings from './GroupSettings.vue';
|
||||
|
||||
import RuleSettings from './RuleSettings.vue';
|
||||
|
||||
const openSettings = ref(false)
|
||||
const settings = function () {
|
||||
|
237
fe/src/components/RuleSettings.vue
Normal file
237
fe/src/components/RuleSettings.vue
Normal file
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<el-table :data="data" :show-header="true">
|
||||
<el-table-column prop="id" label="id" />
|
||||
<el-table-column prop="name" :label="lang.rule_name" />
|
||||
<el-table-column prop="action" :label="lang.rule_do">
|
||||
<template #default="scope">
|
||||
{{ ActionName[scope.row.action] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="params" :label="lang.rule_params" />
|
||||
<el-table-column prop="sort" :label="lang.rule_priority" />
|
||||
<el-table-column>
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-button size="small" type="primary" :icon="Edit" circle @click="editRule(scope.row)" />
|
||||
<el-popconfirm confirm-button-text="Yes" cancel-button-text="No, Thanks" :icon="InfoFilled"
|
||||
@confirm="delRule(scope.row.id)" icon-color="#626AEF" :title="lang.del_rule_confirm">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger" :icon="Delete" circle />
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div>
|
||||
<el-button @click="dialogVisible = true">{{ lang.new_rule }}</el-button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="lang.new_rule" width="60%">
|
||||
<div style="text-align: left; padding-left: 20px;">
|
||||
<el-form v-model="addRuleForm" :inline="true" label-position="top">
|
||||
<el-form-item style="width: 400px;" :label="lang.rule_name">
|
||||
<el-input v-model="addRuleForm.name" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="lang.rule_priority">
|
||||
<el-input v-model="addRuleForm.sort" type="number" oninput="value=value.replace(/[^\-\d]/g, '')" />
|
||||
</el-form-item>
|
||||
<el-divider />
|
||||
<div style="width: 100%;">{{ lang.rule_desc }}</div>
|
||||
<div style="width: 100%;">
|
||||
<div v-for="(rule, index) in addRuleForm.rules">
|
||||
<el-select v-model="rule.field" placeholder="Select">
|
||||
<el-option key="From" :label="lang.from" value="From" />
|
||||
<el-option key="Subject" :label="lang.subject" value="Subject" />
|
||||
<el-option key="To" :label="lang.to" value="To" />
|
||||
<el-option key="Cc" :label="lang.cc" value="Cc" />
|
||||
<el-option key="Content" :label="lang.content" value="Content" />
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="rule.type" placeholder="Select">
|
||||
<el-option key="equal" :label="lang.equal" value="equal" />
|
||||
<el-option key="contains" :label="lang.contains" value="contains" />
|
||||
<el-option key="regex" :label="lang.regex" value="regex" />
|
||||
</el-select>
|
||||
|
||||
<el-input v-model="rule.rule" style="width: 350px;" />
|
||||
<el-button size="small" type="danger" :icon="Delete" @click="removeRuleLine(index)" circle />
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding-top: 7px;">
|
||||
<el-button size="small" type="primary" :icon="Plus" circle @click="addRule()" />
|
||||
</div>
|
||||
<el-divider />
|
||||
<div style="width: 100%;">{{ lang.rule_do }}</div>
|
||||
<el-form-item>
|
||||
<el-select v-model="addRuleForm.action" placeholder="Select" @change="ruleTypeChange()">
|
||||
<el-option key="mark_read" :label="lang.mark_read" :value="READ" />
|
||||
<el-option key="move" :label="lang.move" :value="MOVE" />
|
||||
<el-option key="delete" :label="lang.delete" :value="DELETE" />
|
||||
<el-option key="forward" :label="lang.forward" :value="FORWARD" />
|
||||
</el-select>
|
||||
<el-select v-if="addRuleForm.action == 4" v-model="addRuleForm.params" @click="reflushGroupInfos">
|
||||
<el-option v-for="gp in groupData.list" :key="gp.id" :label="gp.name" :value="gp.id" />
|
||||
</el-select>
|
||||
|
||||
<el-input v-if="addRuleForm.action == 2" v-model="addRuleForm.params" style="width: 250px;"
|
||||
placeholder="Forward Email Address" />
|
||||
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="submitRule()">
|
||||
{{ lang.submit }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import $http from "../http/http";
|
||||
import lang from '../i18n/i18n';
|
||||
import {
|
||||
Plus,
|
||||
Delete,
|
||||
Edit,
|
||||
InfoFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
const data = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const READ = 1
|
||||
const FORWARD = 2
|
||||
const DELETE = 3
|
||||
const MOVE = 4
|
||||
|
||||
const ActionName = {
|
||||
1: lang.mark_read,
|
||||
2: lang.forward,
|
||||
3: lang.delete,
|
||||
4: lang.move
|
||||
}
|
||||
|
||||
const init = function () {
|
||||
$http.post("/api/rule/get").then((res) => {
|
||||
data.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
const groupData = reactive({
|
||||
list: []
|
||||
})
|
||||
|
||||
const reflushGroupInfos = function () {
|
||||
$http.get('/api/group/list').then(res => {
|
||||
groupData.list = res.data
|
||||
for (let i = 0; i < groupData.list.length; i++) {
|
||||
groupData.list[i].id += ""
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
reflushGroupInfos()
|
||||
|
||||
const addRuleForm = reactive({
|
||||
"id": 0,
|
||||
"name": "",
|
||||
"sort": 0,
|
||||
"rules": [
|
||||
{
|
||||
"field": "",
|
||||
"type": "",
|
||||
"rule": ""
|
||||
}
|
||||
],
|
||||
"action": "",
|
||||
"params": ""
|
||||
})
|
||||
|
||||
const delRule = function (id) {
|
||||
$http.post("/api/rule/del", { "id": id }).then((res) => {
|
||||
ElNotification({
|
||||
title: res.errorNo == 0 ? lang.succ : lang.fail,
|
||||
message: res.data,
|
||||
type: res.errorNo == 0 ? 'success' : 'error',
|
||||
})
|
||||
|
||||
init()
|
||||
})
|
||||
}
|
||||
|
||||
const editRule = function (ruleInfo) {
|
||||
addRuleForm.id = ruleInfo.id
|
||||
addRuleForm.name = ruleInfo.name
|
||||
addRuleForm.rules = ruleInfo.rules
|
||||
addRuleForm.action = ruleInfo.action
|
||||
addRuleForm.params = ruleInfo.params
|
||||
addRuleForm.sort = ruleInfo.sort
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const removeRuleLine = function (index) {
|
||||
addRuleForm.rules.splice(index, 1);
|
||||
}
|
||||
|
||||
const addRule = function () {
|
||||
addRuleForm.rules.push(
|
||||
{
|
||||
"field": "",
|
||||
"type": "",
|
||||
"rule": ""
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const submitRule = function () {
|
||||
let api = "/api/rule/add"
|
||||
if (addRuleForm.id > 0) {
|
||||
api = "/api/rule/update"
|
||||
}
|
||||
|
||||
addRuleForm.sort = parseInt(addRuleForm.sort)
|
||||
|
||||
$http.post(api, addRuleForm).then((res) => {
|
||||
if (res.errorNo != 0) {
|
||||
ElNotification({
|
||||
title: lang.fail,
|
||||
message: res.data,
|
||||
type: 'error',
|
||||
})
|
||||
} else {
|
||||
init()
|
||||
dialogVisible.value = false
|
||||
|
||||
addRuleForm.id = 0
|
||||
addRuleForm.name = ""
|
||||
addRuleForm.sort = 0
|
||||
addRuleForm.rules = [
|
||||
{
|
||||
"field": "",
|
||||
"type": "",
|
||||
"rule": ""
|
||||
}
|
||||
]
|
||||
addRuleForm.action = ""
|
||||
addRuleForm.params = ""
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ruleTypeChange = function () {
|
||||
addRuleForm.params = ''
|
||||
}
|
||||
</script>
|
@ -33,6 +33,11 @@ const rules = reactive({
|
||||
})
|
||||
|
||||
const submit = function () {
|
||||
if (ruleForm.new_pwd == ""){
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (ruleForm.new_pwd != ruleForm.new_pwd2) {
|
||||
ElNotification({
|
||||
title: 'Error',
|
||||
|
@ -64,7 +64,28 @@ var lang = {
|
||||
"move_email_confirm": "Are you sure you want to move them?",
|
||||
"del_btn": "Delete",
|
||||
"move_btn": "Move",
|
||||
"read_btn": "Readed"
|
||||
"read_btn": "Readed",
|
||||
"dangerous":"The content of this email is not secure!",
|
||||
"rule_setting":"Rules",
|
||||
"new_rule":"New mail receiving rules",
|
||||
"rule_name":"Rule Name",
|
||||
"rule_priority":"Rule Priority(Larger values are executed first)",
|
||||
"rule_desc":"When a new message arrives that meets all these conditions:",
|
||||
"rule_do":"Do the following:",
|
||||
"from":"From Email Address",
|
||||
"subject":"Email Subject",
|
||||
"to":"Recipient's address",
|
||||
"cc":"Cc's address",
|
||||
"content":"Email Content",
|
||||
"equal":"Equal",
|
||||
"regex":"Regex Match",
|
||||
"contains":"Contains",
|
||||
"mark_read":"Mark Read",
|
||||
"delete":"Delete",
|
||||
"forward":"Forward",
|
||||
"move":"Move to group",
|
||||
"del_rule_confirm":"Are you sure to delete this?",
|
||||
"rule_params":"Executed params",
|
||||
};
|
||||
|
||||
|
||||
@ -135,7 +156,28 @@ var zhCN = {
|
||||
"move_email_confirm": "你确定要移动这些邮件吗?",
|
||||
"del_btn": "删除",
|
||||
"move_btn": "移动",
|
||||
"read_btn": "已读"
|
||||
"read_btn": "已读",
|
||||
"dangerous":"该邮件内容不安全!",
|
||||
"rule_setting":"规则",
|
||||
"new_rule":"新建收信规则",
|
||||
"rule_name":"规则名称",
|
||||
"rule_priority":"优先级(值越大越先执行)",
|
||||
"rule_desc":"以下规则全部满足时:",
|
||||
"rule_do":"执行操作:",
|
||||
"from":"发件人地址",
|
||||
"subject":"邮件主题",
|
||||
"to":"收件人地址",
|
||||
"cc":"抄送地址",
|
||||
"content":"邮件内容",
|
||||
"equal":"等于",
|
||||
"regex":"正则匹配",
|
||||
"contains":"包含",
|
||||
"mark_read":"标记已读",
|
||||
"delete":"删除",
|
||||
"forward":"转发",
|
||||
"move":"移动分组",
|
||||
"del_rule_confirm":"确定要删除吗?",
|
||||
"rule_params":"执行参数",
|
||||
}
|
||||
|
||||
switch (navigator.language) {
|
||||
|
@ -32,6 +32,14 @@
|
||||
<span v-if="!scope.row.is_read">
|
||||
{{ lang.new }}
|
||||
</span>
|
||||
<span style="font-weight: 900;color: #FF0000;" v-if="scope.row.dangerous">
|
||||
<el-tooltip effect="dark"
|
||||
:content="lang.dangerous"
|
||||
placement="top-start">
|
||||
!
|
||||
</el-tooltip>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
@ -28,7 +28,7 @@
|
||||
<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-select :placeholder="lang.db_select_ph" v-model="dbSettings.type" @change="dbSettings.dsn=''">
|
||||
<el-option label="MySQL" value="mysql" />
|
||||
<el-option label="SQLite3" value="sqlite" />
|
||||
</el-select>
|
||||
@ -40,7 +40,7 @@
|
||||
</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-input v-model="dbSettings.dsn" placeholder="./config/pmail.db"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@ -170,7 +170,7 @@ const adminSettings = reactive({
|
||||
|
||||
const dbSettings = reactive({
|
||||
"type": "sqlite",
|
||||
"dsn": "./pmail.db",
|
||||
"dsn": "./config/pmail.db",
|
||||
"lable": ""
|
||||
})
|
||||
|
||||
|
@ -6,8 +6,11 @@
|
||||
"sslType": "0",
|
||||
"SSLPrivateKeyPath": "config/ssl/private.key",
|
||||
"SSLPublicKeyPath": "config/ssl/public.crt",
|
||||
"dbDSN": "./pmail.db",
|
||||
"dbDSN": "./config/pmail.db",
|
||||
"dbType": "sqlite",
|
||||
"spamFilterLevel": 2,
|
||||
"httpPort": 80,
|
||||
"httpsPort": 443,
|
||||
"weChatPushAppId": "",
|
||||
"weChatPushSecret": "",
|
||||
"weChatPushTemplateId": "",
|
||||
|
@ -20,6 +20,10 @@ type Config struct {
|
||||
SSLPublicKeyPath string `json:"SSLPublicKeyPath"`
|
||||
DbDSN string `json:"dbDSN"`
|
||||
DbType string `json:"dbType"`
|
||||
HttpsEnabled int `json:"httpsEnabled"` //后台页面是否启用https,0默认(启用),1启用,2不启用
|
||||
SpamFilterLevel int `json:"spamFilterLevel"` //垃圾邮件过滤级别,0不过滤、1 spf dkim 校验均失败时过滤,2 spf校验不通过时过滤
|
||||
HttpPort int `json:"httpPort"` //http服务端口设置,默认80
|
||||
HttpsPort int `json:"httpsPort"` //https服务端口,默认443
|
||||
WeChatPushAppId string `json:"weChatPushAppId"`
|
||||
WeChatPushSecret string `json:"weChatPushSecret"`
|
||||
WeChatPushTemplateId string `json:"weChatPushTemplateId"`
|
||||
@ -27,7 +31,6 @@ type Config struct {
|
||||
TgBotToken string `json:"tgBotToken"`
|
||||
TgChatId string `json:"tgChatId"`
|
||||
IsInit bool `json:"isInit"`
|
||||
HttpsEnabled int `json:"httpsEnabled"` //后台页面是否启用https,0默认(启用),1启用,2不启用
|
||||
Tables map[string]string `json:"-"`
|
||||
TablesInitData map[string]string `json:"-"`
|
||||
}
|
||||
@ -35,7 +38,7 @@ type Config struct {
|
||||
//go:embed tables/*
|
||||
var tableConfig embed.FS
|
||||
|
||||
const Version = "2.1.2"
|
||||
const Version = "2.2.0"
|
||||
|
||||
const DBTypeMySQL = "mysql"
|
||||
const DBTypeSQLite = "sqlite"
|
||||
|
@ -6,8 +6,11 @@
|
||||
"sslType": "0",
|
||||
"SSLPrivateKeyPath": "config/ssl/private.key",
|
||||
"SSLPublicKeyPath": "config/ssl/public.crt",
|
||||
"dbDSN": "./pmail.db",
|
||||
"dbDSN": "./config/pmail.db",
|
||||
"dbType": "sqlite",
|
||||
"spamFilterLevel": 2,
|
||||
"httpPort": 80,
|
||||
"httpsPort": 443,
|
||||
"weChatPushAppId": "",
|
||||
"weChatPushSecret": "",
|
||||
"weChatPushTemplateId": "",
|
||||
@ -15,5 +18,5 @@
|
||||
"tgChatId": "",
|
||||
"tgBotToken": "",
|
||||
"isInit": true,
|
||||
"httpsEnabled": 2
|
||||
"httpsEnabled": 1
|
||||
}
|
@ -8,6 +8,9 @@
|
||||
"SSLPublicKeyPath": "config/ssl/public.crt",
|
||||
"dbDSN": "root:root@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local",
|
||||
"dbType": "mysql",
|
||||
"spamFilterLevel": 2,
|
||||
"httpPort": 80,
|
||||
"httpsPort": 443,
|
||||
"weChatPushAppId": "",
|
||||
"weChatPushSecret": "",
|
||||
"weChatPushTemplateId": "",
|
||||
|
@ -16,7 +16,7 @@ CREATE table email
|
||||
attachments json COMMENT '附件内容',
|
||||
spf_check tinyint(1) DEFAULT 0 COMMENT '0未校验,1校验通过,2校验未通过',
|
||||
dkim_check tinyint(1) DEFAULT 0 COMMENT '0未校验,1校验通过,2校验未通过',
|
||||
status tinyint(4) NOT NULL DEFAULT 0 COMMENT '0未发送,1已发送,2发送失败',
|
||||
status tinyint(4) NOT NULL DEFAULT 0 COMMENT '0未发送,1已发送,2发送失败,3删除',
|
||||
send_user_id int unsigned NOT NULL DEFAULT 0 COMMENT '发件人用户id',
|
||||
is_read tinyint(1) NOT NULL DEFAULT 0 COMMENT '未读0,已读1',
|
||||
error text COMMENT '错误信息记录',
|
||||
|
10
server/config/tables/mysql/rule.sql
Normal file
10
server/config/tables/mysql/rule.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE `rule`
|
||||
(
|
||||
id INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
|
||||
user_id int NOT NULL DEFAULT 0 COMMENT '用户id',
|
||||
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '规则名称',
|
||||
`value` json NOT NULL COMMENT '规则内容',
|
||||
action int not null default 0 comment '执行动作,1已读,2转发,3删除',
|
||||
params varchar(255) not null default '' comment '执行参数',
|
||||
sort int not null default 0 COMMENT '排序,越大约优先'
|
||||
) COMMENT '收信规则表'
|
10
server/config/tables/sqlite/rule.sql
Normal file
10
server/config/tables/sqlite/rule.sql
Normal file
@ -0,0 +1,10 @@
|
||||
create table rule
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id int,
|
||||
name varchar(255) default '' not null,
|
||||
value json not null,
|
||||
action int default 0 not null,
|
||||
params varchar(255) default '' not null,
|
||||
sort int default 0 not null
|
||||
)
|
@ -4,13 +4,13 @@ import (
|
||||
"fmt"
|
||||
"github.com/spf13/cast"
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/services/attachments"
|
||||
"pmail/utils/context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetAttachments(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func GetAttachments(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
urlInfos := strings.Split(req.RequestURI, "/")
|
||||
if len(urlInfos) != 4 {
|
||||
response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)
|
||||
@ -29,7 +29,7 @@ func GetAttachments(ctx *dto.Context, w http.ResponseWriter, req *http.Request)
|
||||
w.Write(content)
|
||||
}
|
||||
|
||||
func Download(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func Download(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
urlInfos := strings.Split(req.RequestURI, "/")
|
||||
if len(urlInfos) != 5 {
|
||||
response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)
|
||||
|
@ -2,7 +2,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
type HandlerFunc func(*dto.Context, http.ResponseWriter, *http.Request)
|
||||
type HandlerFunc func(*context.Context, http.ResponseWriter, *http.Request)
|
||||
|
@ -5,16 +5,16 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/services/del_email"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
type emailDeleteRequest struct {
|
||||
IDs []int `json:"ids"`
|
||||
}
|
||||
|
||||
func EmailDelete(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func EmailDelete(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%+v", err)
|
||||
|
@ -5,17 +5,17 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/services/auth"
|
||||
"pmail/services/detail"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
type emailDetailRequest struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
func EmailDetail(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func EmailDetail(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%+v", err)
|
||||
|
@ -7,9 +7,9 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/services/list"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
type emailListResponse struct {
|
||||
@ -19,12 +19,13 @@ type emailListResponse struct {
|
||||
}
|
||||
|
||||
type emilItem struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
Datetime string `json:"datetime"`
|
||||
IsRead bool `json:"is_read"`
|
||||
Sender User `json:"sender"`
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
Datetime string `json:"datetime"`
|
||||
IsRead bool `json:"is_read"`
|
||||
Sender User `json:"sender"`
|
||||
Dangerous bool `json:"dangerous"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
@ -39,7 +40,7 @@ type emailRequest struct {
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
func EmailList(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
var lst []*emilItem
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
@ -67,12 +68,13 @@ func EmailList(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
_ = json.Unmarshal([]byte(email.Sender), &sender)
|
||||
|
||||
lst = append(lst, &emilItem{
|
||||
ID: email.Id,
|
||||
Title: email.Subject,
|
||||
Desc: email.Text.String,
|
||||
Datetime: email.SendDate.Format("2006-01-02 15:04:05"),
|
||||
IsRead: email.IsRead == 1,
|
||||
Sender: sender,
|
||||
ID: email.Id,
|
||||
Title: email.Subject,
|
||||
Desc: email.Text.String,
|
||||
Datetime: email.SendDate.Format("2006-01-02 15:04:05"),
|
||||
IsRead: email.IsRead == 1,
|
||||
Sender: sender,
|
||||
Dangerous: email.SPFCheck == 0 && email.DKIMCheck == 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,9 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/services/group"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
type moveRequest struct {
|
||||
@ -15,7 +15,7 @@ type moveRequest struct {
|
||||
IDs []int `json:"ids"`
|
||||
}
|
||||
|
||||
func Move(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func Move(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%+v", err)
|
||||
|
@ -5,16 +5,16 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/services/detail"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
type markReadRequest struct {
|
||||
IDs []int `json:"ids"`
|
||||
}
|
||||
|
||||
func MarkRead(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func MarkRead(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%+v", err)
|
||||
|
@ -8,13 +8,13 @@ import (
|
||||
"net/http"
|
||||
"pmail/config"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/dto/response"
|
||||
"pmail/hooks"
|
||||
"pmail/i18n"
|
||||
"pmail/smtp_server"
|
||||
"pmail/utils/async"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/send"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -43,7 +43,7 @@ type attachment struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func Send(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%+v", err)
|
||||
@ -157,7 +157,7 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
1,
|
||||
1,
|
||||
time.Now(),
|
||||
ctx.UserInfo.ID,
|
||||
ctx.UserID,
|
||||
"",
|
||||
)
|
||||
emailId, _ := sqlRes.LastInsertId()
|
||||
@ -170,7 +170,7 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
async.New(ctx).Process(func(p any) {
|
||||
errMsg := ""
|
||||
err, sendErr := smtp_server.Send(ctx, e)
|
||||
err, sendErr := send.Send(ctx, e)
|
||||
|
||||
as2 := async.New(ctx)
|
||||
for _, hook := range hooks.HookList {
|
||||
|
@ -11,14 +11,15 @@ import (
|
||||
"pmail/i18n"
|
||||
"pmail/services/group"
|
||||
"pmail/utils/array"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
func GetUserGroupList(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func GetUserGroupList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
infos := group.GetGroupList(ctx)
|
||||
response.NewSuccessResponse(infos).FPrint(w)
|
||||
}
|
||||
|
||||
func GetUserGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func GetUserGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
retData := []*group.GroupItem{
|
||||
{
|
||||
@ -50,7 +51,7 @@ type addGroupRequest struct {
|
||||
ParentId int `json:"parent_id"`
|
||||
}
|
||||
|
||||
func AddGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func AddGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
var reqData *addGroupRequest
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
@ -61,7 +62,7 @@ func AddGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
log.WithContext(ctx).Errorf("%+v", err)
|
||||
}
|
||||
|
||||
res, err := db.Instance.Exec(db.WithContext(ctx, "insert into `group` (name,parent_id,user_id) values (?,?,?)"), reqData.Name, reqData.ParentId, ctx.UserInfo.ID)
|
||||
res, err := db.Instance.Exec(db.WithContext(ctx, "insert into `group` (name,parent_id,user_id) values (?,?,?)"), reqData.Name, reqData.ParentId, ctx.UserID)
|
||||
if err != nil {
|
||||
response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w)
|
||||
return
|
||||
@ -78,7 +79,7 @@ type delGroupRequest struct {
|
||||
Id int `json:"id"`
|
||||
}
|
||||
|
||||
func DelGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func DelGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
var reqData *delGroupRequest
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
|
@ -7,11 +7,11 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/i18n"
|
||||
"pmail/models"
|
||||
"pmail/session"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/password"
|
||||
)
|
||||
|
||||
@ -20,7 +20,7 @@ type loginRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func Login(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
|
@ -3,11 +3,11 @@ package controllers
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
func Ping(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func Ping(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
response.NewSuccessResponse("pong").FPrint(w)
|
||||
log.WithContext(ctx).Info("pong")
|
||||
}
|
||||
|
89
server/controllers/rule.go
Normal file
89
server/controllers/rule.go
Normal file
@ -0,0 +1,89 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/i18n"
|
||||
"pmail/services/rule"
|
||||
"pmail/utils/address"
|
||||
"pmail/utils/array"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
func GetRule(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
res := rule.GetAllRules(ctx)
|
||||
response.NewSuccessResponse(res).FPrint(w)
|
||||
}
|
||||
|
||||
func UpsertRule(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
requestBody, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("ReadError:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var data *dto.Rule
|
||||
err = json.Unmarshal(requestBody, &data)
|
||||
if err != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "params error", err).FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Action == dto.FORWARD && !address.IsValidEmailAddress(data.Params) {
|
||||
|
||||
response.NewErrorResponse(response.ParamsError, "ParamsError error", i18n.GetText(ctx.Lang, "invalid_email_address")).FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range data.Rules {
|
||||
if !array.InArray(r.Field, []string{"From", "Subject", "To", "Cc", "Text", "Html", "Content"}) {
|
||||
response.NewErrorResponse(response.ParamsError, "ParamsError error", "params error! Rule Field Error!").FPrint(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = data.Encode().Save(ctx)
|
||||
if err != nil {
|
||||
response.NewErrorResponse(response.ServerError, "server error", err).FPrint(w)
|
||||
return
|
||||
}
|
||||
response.NewSuccessResponse("succ").FPrint(w)
|
||||
}
|
||||
|
||||
type delRuleReq struct {
|
||||
Id int `json:"id"`
|
||||
}
|
||||
|
||||
func DelRule(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
requestBody, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("ReadError:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var data delRuleReq
|
||||
err = json.Unmarshal(requestBody, &data)
|
||||
if err != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "params error", err).FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Id <= 0 {
|
||||
response.NewErrorResponse(response.ParamsError, "params error", "id is empty").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.Instance.Exec(db.WithContext(ctx, "delete from rule where id =? and user_id =?"), data.Id, ctx.UserID)
|
||||
if err != nil {
|
||||
response.NewErrorResponse(response.ServerError, "unknown error", err).FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
response.NewSuccessResponse("succ").FPrint(w)
|
||||
}
|
@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/i18n"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/password"
|
||||
)
|
||||
|
||||
@ -16,7 +16,7 @@ type modifyPasswordRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func ModifyPassword(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.Errorf("%+v", err)
|
||||
@ -30,7 +30,7 @@ func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request)
|
||||
if retData.Password != "" {
|
||||
encodePwd := password.Encode(retData.Password)
|
||||
|
||||
_, err := db.Instance.Exec(db.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID)
|
||||
_, err := db.Instance.Exec(db.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserID)
|
||||
if err != nil {
|
||||
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
||||
return
|
||||
|
@ -5,10 +5,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/config"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/services/setup"
|
||||
"pmail/services/setup/ssl"
|
||||
"pmail/utils/context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -23,7 +23,7 @@ func AcmeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
|
||||
func Setup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
response.NewSuccessResponse("").FPrint(w)
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "modernc.org/sqlite"
|
||||
"pmail/config"
|
||||
"pmail/dto"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/errors"
|
||||
"strings"
|
||||
)
|
||||
@ -38,9 +38,9 @@ func Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithContext(ctx *dto.Context, sql string) string {
|
||||
func WithContext(ctx *context.Context, sql string) string {
|
||||
if ctx != nil {
|
||||
logId := ctx.GetValue(dto.LogID)
|
||||
logId := ctx.GetValue(context.LogID)
|
||||
return fmt.Sprintf("/* %s */ %s", logId, sql)
|
||||
}
|
||||
return sql
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/textproto"
|
||||
"pmail/dto"
|
||||
"pmail/utils/array"
|
||||
"pmail/utils/context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@ -42,6 +42,9 @@ type Email struct {
|
||||
Attachments []*Attachment
|
||||
ReadReceipt []string
|
||||
Date string
|
||||
IsRead int
|
||||
Status int // 0未发送,1已发送,2发送失败,3删除
|
||||
GroupId int // 分组id
|
||||
}
|
||||
|
||||
func NewEmailFromReader(r io.Reader) *Email {
|
||||
@ -166,7 +169,85 @@ func buildUsers(str []string) []*User {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (e *Email) BuildBytes(ctx *dto.Context) []byte {
|
||||
func (e *Email) ForwardBuildBytes(ctx *context.Context, forwardAddress string) []byte {
|
||||
var b bytes.Buffer
|
||||
|
||||
from := []*mail.Address{{e.From.Name, e.From.EmailAddress}}
|
||||
to := []*mail.Address{
|
||||
{
|
||||
Address: forwardAddress,
|
||||
},
|
||||
}
|
||||
|
||||
// Create our mail header
|
||||
var h mail.Header
|
||||
h.SetDate(time.Now())
|
||||
h.SetAddressList("From", from)
|
||||
h.SetAddressList("To", to)
|
||||
h.SetText("Subject", e.Subject)
|
||||
if len(e.Cc) != 0 {
|
||||
cc := []*mail.Address{}
|
||||
for _, user := range e.Cc {
|
||||
cc = append(cc, &mail.Address{
|
||||
Name: user.Name,
|
||||
Address: user.EmailAddress,
|
||||
})
|
||||
}
|
||||
h.SetAddressList("Cc", cc)
|
||||
}
|
||||
|
||||
// Create a new mail writer
|
||||
mw, err := mail.CreateWriter(&b, h)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Fatal(err)
|
||||
}
|
||||
|
||||
// Create a text part
|
||||
tw, err := mw.CreateInline()
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Fatal(err)
|
||||
}
|
||||
var th mail.InlineHeader
|
||||
th.Set("Content-Type", "text/plain")
|
||||
w, err := tw.CreatePart(th)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
io.WriteString(w, string(e.Text))
|
||||
w.Close()
|
||||
|
||||
var html mail.InlineHeader
|
||||
html.Set("Content-Type", "text/html")
|
||||
w, err = tw.CreatePart(html)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
io.WriteString(w, string(e.HTML))
|
||||
w.Close()
|
||||
|
||||
tw.Close()
|
||||
|
||||
// Create an attachment
|
||||
for _, attachment := range e.Attachments {
|
||||
var ah mail.AttachmentHeader
|
||||
ah.Set("Content-Type", attachment.ContentType)
|
||||
ah.SetFilename(attachment.Filename)
|
||||
w, err = mw.CreateAttachment(ah)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Fatal(err)
|
||||
continue
|
||||
}
|
||||
w.Write(attachment.Content)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
mw.Close()
|
||||
|
||||
// dkim 签名后返回
|
||||
return instance.Sign(b.String())
|
||||
}
|
||||
|
||||
func (e *Email) BuildBytes(ctx *context.Context) []byte {
|
||||
var b bytes.Buffer
|
||||
|
||||
from := []*mail.Address{{e.From.Name, e.From.EmailAddress}}
|
||||
|
54
server/dto/rule.go
Normal file
54
server/dto/rule.go
Normal file
@ -0,0 +1,54 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"pmail/models"
|
||||
)
|
||||
|
||||
type RuleType int
|
||||
|
||||
// 1已读,2转发,3删除
|
||||
var (
|
||||
READ RuleType = 1
|
||||
FORWARD RuleType = 2
|
||||
DELETE RuleType = 3
|
||||
MOVE RuleType = 4
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Rules []*Value `json:"rules"`
|
||||
Action RuleType `json:"action"`
|
||||
Params string `json:"params"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
type Value struct {
|
||||
Field string `json:"field"`
|
||||
Type string `json:"type"`
|
||||
Rule string `json:"rule"`
|
||||
}
|
||||
|
||||
func (p *Rule) Decode(data *models.Rule) *Rule {
|
||||
json.Unmarshal([]byte(data.Value), &p.Rules)
|
||||
p.Id = data.Id
|
||||
p.Name = data.Name
|
||||
p.Action = RuleType(data.Action)
|
||||
p.Sort = data.Sort
|
||||
p.Params = data.Params
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Rule) Encode() *models.Rule {
|
||||
v, _ := json.Marshal(p.Rules)
|
||||
ret := &models.Rule{
|
||||
Id: p.Id,
|
||||
Name: p.Name,
|
||||
Value: string(v),
|
||||
Action: int(p.Action),
|
||||
Sort: p.Sort,
|
||||
Params: p.Params,
|
||||
}
|
||||
return ret
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"pmail/dto"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/hooks/telegram_push"
|
||||
"pmail/hooks/wechat_push"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
type EmailHook interface {
|
||||
// SendBefore 邮件发送前的数据
|
||||
SendBefore(ctx *dto.Context, email *parsemail.Email)
|
||||
SendBefore(ctx *context.Context, email *parsemail.Email)
|
||||
// SendAfter 邮件发送后的数据,err是每个收信服务器的错误信息
|
||||
SendAfter(ctx *dto.Context, email *parsemail.Email, err map[string]error)
|
||||
SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error)
|
||||
// ReceiveParseBefore 接收到邮件,解析之前的原始数据
|
||||
ReceiveParseBefore(email []byte)
|
||||
// ReceiveParseAfter 接收到邮件,解析之后的结构化数据
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"pmail/config"
|
||||
"pmail/dto"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/utils/context"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -19,11 +19,11 @@ type TelegramPushHook struct {
|
||||
webDomain string
|
||||
}
|
||||
|
||||
func (w *TelegramPushHook) SendBefore(ctx *dto.Context, email *parsemail.Email) {
|
||||
func (w *TelegramPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
|
||||
|
||||
}
|
||||
|
||||
func (w *TelegramPushHook) SendAfter(ctx *dto.Context, email *parsemail.Email, err map[string]error) {
|
||||
func (w *TelegramPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
|
||||
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ type InlineKeyboardButton struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (w *TelegramPushHook) sendUserMsg(ctx *dto.Context, email *parsemail.Email) {
|
||||
func (w *TelegramPushHook) sendUserMsg(ctx *context.Context, email *parsemail.Email) {
|
||||
url := w.webDomain
|
||||
if w.httpsEnabled > 1 {
|
||||
url = "http://" + url
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/config"
|
||||
"pmail/dto"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/utils/context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -28,11 +28,11 @@ type WeChatPushHook struct {
|
||||
pushUser string
|
||||
}
|
||||
|
||||
func (w *WeChatPushHook) SendBefore(ctx *dto.Context, email *parsemail.Email) {
|
||||
func (w *WeChatPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
|
||||
|
||||
}
|
||||
|
||||
func (w *WeChatPushHook) SendAfter(ctx *dto.Context, email *parsemail.Email, err map[string]error) {
|
||||
func (w *WeChatPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
|
||||
|
||||
}
|
||||
|
||||
@ -45,7 +45,13 @@ func (w *WeChatPushHook) ReceiveParseAfter(email *parsemail.Email) {
|
||||
return
|
||||
}
|
||||
|
||||
w.sendUserMsg(nil, w.pushUser, string(email.Text))
|
||||
content := string(email.Text)
|
||||
|
||||
if content == "" {
|
||||
content = email.Subject
|
||||
}
|
||||
|
||||
w.sendUserMsg(nil, w.pushUser, content)
|
||||
}
|
||||
|
||||
func (w *WeChatPushHook) getWxAccessToken() string {
|
||||
@ -80,11 +86,19 @@ type DataItem struct {
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
func (w *WeChatPushHook) sendUserMsg(ctx *dto.Context, userId string, content string) {
|
||||
func (w *WeChatPushHook) sendUserMsg(ctx *context.Context, userId string, content string) {
|
||||
|
||||
url := config.Instance.WebDomain
|
||||
if config.Instance.HttpsEnabled > 1 {
|
||||
url = "http://" + url
|
||||
} else {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
sendMsgReq, _ := json.Marshal(sendMsgRequest{
|
||||
Touser: userId,
|
||||
Template_id: w.templateId,
|
||||
Url: "http://mail." + config.Instance.Domain,
|
||||
Url: url,
|
||||
Data: SendData{Content: DataItem{Value: content, Color: "#000000"}},
|
||||
})
|
||||
|
||||
|
@ -11,8 +11,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const HttpPort = 80
|
||||
|
||||
// 这个服务是为了拦截http请求转发到https
|
||||
var httpServer *http.Server
|
||||
|
||||
@ -25,6 +23,11 @@ func HttpStop() {
|
||||
func HttpStart() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
HttpPort := 80
|
||||
if config.Instance.HttpPort > 0 {
|
||||
HttpPort = config.Instance.HttpPort
|
||||
}
|
||||
|
||||
if config.Instance.HttpsEnabled != 2 {
|
||||
mux.HandleFunc("/", controllers.Interceptor)
|
||||
httpServer = &http.Server{
|
||||
@ -52,6 +55,10 @@ func HttpStart() {
|
||||
mux.HandleFunc("/api/email/move", contextIterceptor(email.Move))
|
||||
mux.HandleFunc("/api/email/send", contextIterceptor(email.Send))
|
||||
mux.HandleFunc("/api/settings/modify_password", contextIterceptor(controllers.ModifyPassword))
|
||||
mux.HandleFunc("/api/rule/get", contextIterceptor(controllers.GetRule))
|
||||
mux.HandleFunc("/api/rule/add", contextIterceptor(controllers.UpsertRule))
|
||||
mux.HandleFunc("/api/rule/update", contextIterceptor(controllers.UpsertRule))
|
||||
mux.HandleFunc("/api/rule/del", contextIterceptor(controllers.DelRule))
|
||||
mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
|
||||
mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
|
||||
httpServer = &http.Server{
|
||||
|
@ -17,18 +17,17 @@ import (
|
||||
"pmail/config"
|
||||
"pmail/controllers"
|
||||
"pmail/controllers/email"
|
||||
"pmail/dto"
|
||||
"pmail/dto/response"
|
||||
"pmail/i18n"
|
||||
"pmail/models"
|
||||
"pmail/session"
|
||||
"pmail/utils/context"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
var local embed.FS
|
||||
|
||||
const HttpsPort = 443
|
||||
|
||||
var httpsServer *http.Server
|
||||
|
||||
type nullWrite struct {
|
||||
@ -62,12 +61,21 @@ func HttpsStart() {
|
||||
mux.HandleFunc("/api/email/send", contextIterceptor(email.Send))
|
||||
mux.HandleFunc("/api/email/move", contextIterceptor(email.Move))
|
||||
mux.HandleFunc("/api/settings/modify_password", contextIterceptor(controllers.ModifyPassword))
|
||||
mux.HandleFunc("/api/rule/get", contextIterceptor(controllers.GetRule))
|
||||
mux.HandleFunc("/api/rule/add", contextIterceptor(controllers.UpsertRule))
|
||||
mux.HandleFunc("/api/rule/update", contextIterceptor(controllers.UpsertRule))
|
||||
mux.HandleFunc("/api/rule/del", contextIterceptor(controllers.DelRule))
|
||||
mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
|
||||
mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
|
||||
|
||||
// go http server会打一堆没用的日志,写一个空的日志处理器,屏蔽掉日志输出
|
||||
nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
|
||||
|
||||
HttpsPort := 443
|
||||
if config.Instance.HttpsPort > 0 {
|
||||
HttpsPort = config.Instance.HttpsPort
|
||||
}
|
||||
|
||||
if config.Instance.HttpsEnabled != 2 {
|
||||
httpsServer = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", HttpsPort),
|
||||
@ -117,9 +125,9 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
ctx := &dto.Context{}
|
||||
ctx := &context.Context{}
|
||||
ctx.Context = r.Context()
|
||||
ctx.SetValue(dto.LogID, genLogID())
|
||||
ctx.SetValue(context.LogID, genLogID())
|
||||
lang := r.Header.Get("Lang")
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
@ -128,10 +136,17 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
if config.IsInit {
|
||||
user := cast.ToString(session.Instance.Get(ctx, "user"))
|
||||
var userInfo *models.User
|
||||
if user != "" {
|
||||
_ = json.Unmarshal([]byte(user), &ctx.UserInfo)
|
||||
_ = json.Unmarshal([]byte(user), &userInfo)
|
||||
}
|
||||
if ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
|
||||
if userInfo != nil && userInfo.ID > 0 {
|
||||
ctx.UserID = userInfo.ID
|
||||
ctx.UserName = userInfo.Name
|
||||
ctx.UserAccount = userInfo.Account
|
||||
}
|
||||
|
||||
if ctx.UserID == 0 {
|
||||
if r.URL.Path != "/api/ping" && r.URL.Path != "/api/login" {
|
||||
response.NewErrorResponse(response.NeedLogin, i18n.GetText(ctx.Lang, "login_exp"), "").FPrint(w)
|
||||
return
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"pmail/config"
|
||||
"pmail/controllers"
|
||||
"time"
|
||||
)
|
||||
@ -25,6 +26,11 @@ func SetupStart() {
|
||||
// 挑战请求类似这样 /.well-known/acme-challenge/QPyMAyaWw9s5JvV1oruyqWHG7OqkHMJEHPoUz2046KM
|
||||
mux.HandleFunc("/.well-known/", controllers.AcmeChallenge)
|
||||
|
||||
HttpPort := 80
|
||||
if config.Instance != nil && config.Instance.HttpPort > 0 {
|
||||
HttpPort = config.Instance.HttpPort
|
||||
}
|
||||
|
||||
setupServer = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", HttpPort),
|
||||
Handler: mux,
|
||||
|
@ -2,30 +2,32 @@ package i18n
|
||||
|
||||
var (
|
||||
cn = map[string]string{
|
||||
"all_email": "全部邮件数据",
|
||||
"inbox": "收件箱",
|
||||
"outbox": "发件箱",
|
||||
"sketch": "草稿箱",
|
||||
"aperror": "账号或密码错误",
|
||||
"unknowError": "未知错误",
|
||||
"succ": "成功",
|
||||
"send_fail": "发送失败",
|
||||
"att_err": "附件解码错误",
|
||||
"login_exp": "登录已失效",
|
||||
"ip_taps": "这是你服务器IP,确保这个IP正确",
|
||||
"all_email": "全部邮件数据",
|
||||
"inbox": "收件箱",
|
||||
"outbox": "发件箱",
|
||||
"sketch": "草稿箱",
|
||||
"aperror": "账号或密码错误",
|
||||
"unknowError": "未知错误",
|
||||
"succ": "成功",
|
||||
"send_fail": "发送失败",
|
||||
"att_err": "附件解码错误",
|
||||
"login_exp": "登录已失效",
|
||||
"ip_taps": "这是你服务器IP,确保这个IP正确",
|
||||
"invalid_email_address": "无效的邮箱地址!",
|
||||
}
|
||||
en = map[string]string{
|
||||
"all_email": "All Email",
|
||||
"inbox": "Inbox",
|
||||
"outbox": "Outbox",
|
||||
"sketch": "Sketch",
|
||||
"aperror": "Incorrect account number or password",
|
||||
"unknowError": "Unknow Error",
|
||||
"succ": "Success",
|
||||
"send_fail": "Send Failure",
|
||||
"att_err": "Attachment decoding error",
|
||||
"login_exp": "Login has expired.",
|
||||
"ip_taps": "This is your server's IP, make sure it is correct.",
|
||||
"all_email": "All Email",
|
||||
"inbox": "Inbox",
|
||||
"outbox": "Outbox",
|
||||
"sketch": "Sketch",
|
||||
"aperror": "Incorrect account number or password",
|
||||
"unknowError": "Unknow Error",
|
||||
"succ": "Success",
|
||||
"send_fail": "Send Failure",
|
||||
"att_err": "Attachment decoding error",
|
||||
"login_exp": "Login has expired.",
|
||||
"ip_taps": "This is your server's IP, make sure it is correct.",
|
||||
"invalid_email_address": "Invalid e-mail address!",
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -6,8 +6,9 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"pmail/config"
|
||||
"pmail/dto"
|
||||
"pmail/cron_server"
|
||||
"pmail/res_init"
|
||||
"pmail/utils/context"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -21,7 +22,7 @@ func (l *logFormatter) Format(entry *log.Entry) ([]byte, error) {
|
||||
b.WriteString(fmt.Sprintf("[%s]", entry.Level.String()))
|
||||
b.WriteString(fmt.Sprintf("[%s]", entry.Time.Format("2006-01-02 15:04:05")))
|
||||
if entry.Context != nil {
|
||||
b.WriteString(fmt.Sprintf("[%s]", entry.Context.(*dto.Context).GetValue(dto.LogID)))
|
||||
b.WriteString(fmt.Sprintf("[%s]", entry.Context.(*context.Context).GetValue(context.LogID)))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("[%s:%d]", entry.Caller.File, entry.Caller.Line))
|
||||
b.WriteString(entry.Message)
|
||||
@ -38,8 +39,6 @@ var (
|
||||
|
||||
func main() {
|
||||
// 设置日志格式为json格式
|
||||
//log.SetFormatter(&log.JSONFormatter{})
|
||||
|
||||
log.SetFormatter(&logFormatter{})
|
||||
log.SetReportCaller(true)
|
||||
|
||||
@ -79,7 +78,7 @@ func main() {
|
||||
log.Infoln("***************************************************")
|
||||
|
||||
// 定时任务启动
|
||||
//go cron_server.Start()
|
||||
go cron_server.Start()
|
||||
|
||||
// 核心服务启动
|
||||
res_init.Init()
|
||||
|
@ -23,7 +23,7 @@ type Email struct {
|
||||
Attachments string `db:"attachments" json:"attachments"`
|
||||
SPFCheck int8 `db:"spf_check" json:"spf_check"`
|
||||
DKIMCheck int8 `db:"dkim_check" json:"dkim_check"`
|
||||
Status int8 `db:"status" json:"status"`
|
||||
Status int8 `db:"status" json:"status"` // 0未发送,1已发送,2发送失败,3删除
|
||||
CronSendTime time.Time `db:"cron_send_time" json:"cron_send_time"`
|
||||
UpdateTime time.Time `db:"update_time" json:"update_time"`
|
||||
SendUserID int `db:"send_user_id" json:"send_user_id"`
|
||||
|
35
server/models/rule.go
Normal file
35
server/models/rule.go
Normal file
@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"pmail/db"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/errors"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Id int `db:"id" json:"id"`
|
||||
UserId string `db:"user_id" json:"user_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Value string `db:"value" json:"value"`
|
||||
Action int `db:"action" json:"action"`
|
||||
Params string `db:"params" json:"params"`
|
||||
Sort int `db:"sort" json:"sort"`
|
||||
}
|
||||
|
||||
func (p *Rule) Save(ctx *context.Context) error {
|
||||
|
||||
if p.Id > 0 {
|
||||
_, err := db.Instance.Exec(db.WithContext(ctx, "update rule set name=? ,value = ? ,action = ?,params = ?,sort = ? where id = ?"), p.Name, p.Value, p.Action, p.Params, p.Sort, p.Id)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
_, err := db.Instance.Exec(db.WithContext(ctx, "insert into rule (name,value,user_id,action,params,sort) values (?,?,?,?,?,?)"), p.Name, p.Value, ctx.UserID, p.Action, p.Params, p.Sort)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
@ -4,13 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/models"
|
||||
"pmail/services/auth"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
func GetAttachments(ctx *dto.Context, emailId int, cid string) (string, []byte) {
|
||||
func GetAttachments(ctx *context.Context, emailId int, cid string) (string, []byte) {
|
||||
|
||||
// 获取邮件内容
|
||||
var email models.Email
|
||||
@ -35,7 +35,7 @@ func GetAttachments(ctx *dto.Context, emailId int, cid string) (string, []byte)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func GetAttachmentsByIndex(ctx *dto.Context, emailId int, index int) (string, []byte) {
|
||||
func GetAttachmentsByIndex(ctx *context.Context, emailId int, index int) (string, []byte) {
|
||||
|
||||
// 获取邮件内容
|
||||
var email models.Email
|
||||
|
@ -10,16 +10,16 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/models"
|
||||
"pmail/utils/context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HasAuth 检查当前用户是否有某个邮件的auth
|
||||
func HasAuth(ctx *dto.Context, email *models.Email) bool {
|
||||
func HasAuth(ctx *context.Context, email *models.Email) bool {
|
||||
// 获取当前用户的auth
|
||||
var auth []models.UserAuth
|
||||
err := db.Instance.Select(&auth, db.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.UserID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("SQL error:%+v", err)
|
||||
return false
|
||||
@ -30,7 +30,7 @@ func HasAuth(ctx *dto.Context, email *models.Email) bool {
|
||||
if userAuth.EmailAccount == "*" {
|
||||
hasAuth = true
|
||||
break
|
||||
} else if strings.Contains(email.Bcc, ctx.UserInfo.Account) || strings.Contains(email.Cc, ctx.UserInfo.Account) || strings.Contains(email.To, ctx.UserInfo.Account) {
|
||||
} else if strings.Contains(email.Bcc, ctx.UserAccount) || strings.Contains(email.Cc, ctx.UserAccount) || strings.Contains(email.To, ctx.UserAccount) {
|
||||
hasAuth = true
|
||||
break
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ package del_email
|
||||
import (
|
||||
"fmt"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/models"
|
||||
"pmail/services/auth"
|
||||
"pmail/utils/array"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/errors"
|
||||
)
|
||||
|
||||
func DelEmail(ctx *dto.Context, ids []int) error {
|
||||
func DelEmail(ctx *context.Context, ids []int) error {
|
||||
var emails []*models.Email
|
||||
|
||||
db.Instance.Select(&emails, db.WithContext(ctx, fmt.Sprintf("select * from email where id in (%s)", array.Join(ids, ","))))
|
||||
@ -23,7 +23,8 @@ func DelEmail(ctx *dto.Context, ids []int) error {
|
||||
}
|
||||
}
|
||||
|
||||
_, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("delete from email where id in (%s)", array.Join(ids, ","))))
|
||||
//_, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("delete from email where id in (%s)", array.Join(ids, ","))))
|
||||
_, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("update email set status = 3 where id in (%s)", array.Join(ids, ","))))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
@ -6,13 +6,13 @@ import (
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/models"
|
||||
"pmail/utils/context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetEmailDetail(ctx *dto.Context, id int, markRead bool) (*models.Email, error) {
|
||||
func GetEmailDetail(ctx *context.Context, id int, markRead bool) (*models.Email, error) {
|
||||
// 获取邮件内容
|
||||
var email models.Email
|
||||
err := db.Instance.Get(&email, db.WithContext(ctx, "select * from email where id = ?"), id)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"pmail/dto"
|
||||
"pmail/models"
|
||||
"pmail/utils/array"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/errors"
|
||||
)
|
||||
|
||||
@ -17,7 +18,7 @@ type GroupItem struct {
|
||||
Children []*GroupItem `json:"children"`
|
||||
}
|
||||
|
||||
func DelGroup(ctx *dto.Context, groupId int) (bool, error) {
|
||||
func DelGroup(ctx *context.Context, groupId int) (bool, error) {
|
||||
allGroupIds := getAllChildId(ctx, groupId)
|
||||
allGroupIds = append(allGroupIds, groupId)
|
||||
|
||||
@ -27,7 +28,7 @@ func DelGroup(ctx *dto.Context, groupId int) (bool, error) {
|
||||
return false, errors.Wrap(err)
|
||||
}
|
||||
|
||||
res, err := trans.Exec(db.WithContext(ctx, fmt.Sprintf("delete from `group` where id in (%s) and user_id =?", array.Join(allGroupIds, ","))), ctx.UserInfo.ID)
|
||||
res, err := trans.Exec(db.WithContext(ctx, fmt.Sprintf("delete from `group` where id in (%s) and user_id =?", array.Join(allGroupIds, ","))), ctx.UserID)
|
||||
if err != nil {
|
||||
trans.Rollback()
|
||||
return false, errors.Wrap(err)
|
||||
@ -53,10 +54,10 @@ type id struct {
|
||||
Id int `db:"id"`
|
||||
}
|
||||
|
||||
func getAllChildId(ctx *dto.Context, rootId int) []int {
|
||||
func getAllChildId(ctx *context.Context, rootId int) []int {
|
||||
var ids []id
|
||||
var ret []int
|
||||
db.Instance.Select(&ids, db.WithContext(ctx, "select id from `group` where parent_id=? and user_id=?"), rootId, ctx.UserInfo.ID)
|
||||
db.Instance.Select(&ids, db.WithContext(ctx, "select id from `group` where parent_id=? and user_id=?"), rootId, ctx.UserID)
|
||||
for _, item := range ids {
|
||||
ret = array.Merge(ret, getAllChildId(ctx, item.Id))
|
||||
ret = append(ret, item.Id)
|
||||
@ -65,12 +66,12 @@ func getAllChildId(ctx *dto.Context, rootId int) []int {
|
||||
}
|
||||
|
||||
// GetGroupInfoList 获取全部的分组
|
||||
func GetGroupInfoList(ctx *dto.Context) []*GroupItem {
|
||||
func GetGroupInfoList(ctx *context.Context) []*GroupItem {
|
||||
return buildChildren(ctx, 0)
|
||||
}
|
||||
|
||||
// MoveMailToGroup 将某封邮件移动到某个分组中
|
||||
func MoveMailToGroup(ctx *dto.Context, mailId []int, groupId int) bool {
|
||||
func MoveMailToGroup(ctx *context.Context, mailId []int, groupId int) bool {
|
||||
res, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("update email set group_id=? where id in (%s)", array.Join(mailId, ","))), groupId)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("SQL Error:%+v", err)
|
||||
@ -85,10 +86,10 @@ func MoveMailToGroup(ctx *dto.Context, mailId []int, groupId int) bool {
|
||||
return rowNum > 0
|
||||
}
|
||||
|
||||
func buildChildren(ctx *dto.Context, parentId int) []*GroupItem {
|
||||
func buildChildren(ctx *context.Context, parentId int) []*GroupItem {
|
||||
var ret []*GroupItem
|
||||
var rootGroup []*models.Group
|
||||
err := db.Instance.Select(&rootGroup, db.WithContext(ctx, "select * from `group` where parent_id=? and user_id=?"), parentId, ctx.UserInfo.ID)
|
||||
err := db.Instance.Select(&rootGroup, db.WithContext(ctx, "select * from `group` where parent_id=? and user_id=?"), parentId, ctx.UserID)
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("SQL Error:%v", err)
|
||||
@ -107,8 +108,8 @@ func buildChildren(ctx *dto.Context, parentId int) []*GroupItem {
|
||||
|
||||
}
|
||||
|
||||
func GetGroupList(ctx *dto.Context) []*models.Group {
|
||||
func GetGroupList(ctx *context.Context) []*models.Group {
|
||||
var ret []*models.Group
|
||||
db.Instance.Select(&ret, db.WithContext(ctx, "select * from `group` where user_id=?"), ctx.UserInfo.ID)
|
||||
db.Instance.Select(&ret, db.WithContext(ctx, "select * from `group` where user_id=?"), ctx.UserID)
|
||||
return ret
|
||||
}
|
||||
|
@ -6,19 +6,20 @@ import (
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/models"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit int) (emailList []*models.Email, total int) {
|
||||
func GetEmailList(ctx *context.Context, tag string, keyword string, offset, limit int) (emailList []*models.Email, total int) {
|
||||
|
||||
querySQL, queryParams := genSQL(ctx, false, tag, keyword, offset, limit)
|
||||
counterSQL, counterParams := genSQL(ctx, true, tag, keyword, offset, limit)
|
||||
|
||||
err := db.Instance.Select(&emailList, querySQL, queryParams...)
|
||||
err := db.Instance.Select(&emailList, db.WithContext(ctx, querySQL), queryParams...)
|
||||
if err != nil {
|
||||
log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
|
||||
}
|
||||
|
||||
err = db.Instance.Get(&total, counterSQL, counterParams...)
|
||||
err = db.Instance.Get(&total, db.WithContext(ctx, counterSQL), counterParams...)
|
||||
if err != nil {
|
||||
log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
|
||||
}
|
||||
@ -26,7 +27,7 @@ func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit in
|
||||
return
|
||||
}
|
||||
|
||||
func genSQL(ctx *dto.Context, counter bool, tag, keyword string, offset, limit int) (string, []any) {
|
||||
func genSQL(ctx *context.Context, counter bool, tag, keyword string, offset, limit int) (string, []any) {
|
||||
|
||||
sql := "select * from email where 1=1 "
|
||||
if counter {
|
||||
@ -46,6 +47,8 @@ func genSQL(ctx *dto.Context, counter bool, tag, keyword string, offset, limit i
|
||||
if tagInfo.Status != -1 {
|
||||
sql += " and status =? "
|
||||
sqlParams = append(sqlParams, tagInfo.Status)
|
||||
} else {
|
||||
sql += " and status != 3"
|
||||
}
|
||||
|
||||
if tagInfo.GroupId != -1 {
|
||||
@ -58,7 +61,7 @@ func genSQL(ctx *dto.Context, counter bool, tag, keyword string, offset, limit i
|
||||
sqlParams = append(sqlParams, "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
sql += " limit ? offset ?"
|
||||
sql += " order by id desc limit ? offset ?"
|
||||
sqlParams = append(sqlParams, limit, offset)
|
||||
|
||||
return sql, sqlParams
|
||||
|
51
server/services/rule/match/base.go
Normal file
51
server/services/rule/match/base.go
Normal file
@ -0,0 +1,51 @@
|
||||
package match
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
const (
|
||||
RuleTypeRegex = "regex"
|
||||
RuleTypeContains = "contains"
|
||||
RuleTypeEq = "equal"
|
||||
)
|
||||
|
||||
type Match interface {
|
||||
Match(ctx *context.Context, email *parsemail.Email) bool
|
||||
}
|
||||
|
||||
func getFieldContent(field string, email *parsemail.Email) string {
|
||||
switch field {
|
||||
case "ReplyTo":
|
||||
b, _ := json.Marshal(email.ReplyTo)
|
||||
return string(b)
|
||||
case "From":
|
||||
b, _ := json.Marshal(email.From)
|
||||
return string(b)
|
||||
case "Subject":
|
||||
return email.Subject
|
||||
case "To":
|
||||
b, _ := json.Marshal(email.To)
|
||||
return string(b)
|
||||
case "Bcc":
|
||||
b, _ := json.Marshal(email.Bcc)
|
||||
return string(b)
|
||||
case "Cc":
|
||||
b, _ := json.Marshal(email.Cc)
|
||||
return string(b)
|
||||
case "Text":
|
||||
return string(email.Text)
|
||||
case "Html":
|
||||
return string(email.HTML)
|
||||
case "Sender":
|
||||
b, _ := json.Marshal(email.Sender)
|
||||
return string(b)
|
||||
case "Content":
|
||||
b := string(email.HTML)
|
||||
b2 := string(email.Text)
|
||||
return b + b2
|
||||
}
|
||||
return ""
|
||||
}
|
24
server/services/rule/match/contains_match.go
Normal file
24
server/services/rule/match/contains_match.go
Normal file
@ -0,0 +1,24 @@
|
||||
package match
|
||||
|
||||
import (
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/utils/context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ContainsMatch struct {
|
||||
Rule string
|
||||
Field string
|
||||
}
|
||||
|
||||
func NewContainsMatch(field, rule string) *ContainsMatch {
|
||||
return &ContainsMatch{
|
||||
Rule: rule,
|
||||
Field: field,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ContainsMatch) Match(ctx *context.Context, email *parsemail.Email) bool {
|
||||
content := getFieldContent(r.Field, email)
|
||||
return strings.Contains(content, r.Rule)
|
||||
}
|
23
server/services/rule/match/equal_match.go
Normal file
23
server/services/rule/match/equal_match.go
Normal file
@ -0,0 +1,23 @@
|
||||
package match
|
||||
|
||||
import (
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/utils/context"
|
||||
)
|
||||
|
||||
type EqualMatch struct {
|
||||
Rule string
|
||||
Field string
|
||||
}
|
||||
|
||||
func NewEqualMatch(field, rule string) *EqualMatch {
|
||||
return &EqualMatch{
|
||||
Rule: rule,
|
||||
Field: field,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *EqualMatch) Match(ctx *context.Context, email *parsemail.Email) bool {
|
||||
content := getFieldContent(r.Field, email)
|
||||
return content == r.Rule
|
||||
}
|
32
server/services/rule/match/regex_match.go
Normal file
32
server/services/rule/match/regex_match.go
Normal file
@ -0,0 +1,32 @@
|
||||
package match
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/utils/context"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type RegexMatch struct {
|
||||
Rule string
|
||||
Field string
|
||||
}
|
||||
|
||||
func NewRegexMatch(field, rule string) *RegexMatch {
|
||||
return &RegexMatch{
|
||||
Rule: rule,
|
||||
Field: field,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegexMatch) Match(ctx *context.Context, email *parsemail.Email) bool {
|
||||
content := getFieldContent(r.Field, email)
|
||||
|
||||
match, err := regexp.MatchString(r.Rule, content)
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("rule regex error %v", err)
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
18
server/services/rule/match/regex_match_test.go
Normal file
18
server/services/rule/match/regex_match_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
package match
|
||||
|
||||
import (
|
||||
"pmail/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegexMatch_Match(t *testing.T) {
|
||||
r := NewRegexMatch("Subject", "\\d+")
|
||||
|
||||
ret := r.Match(nil, &models.Email{
|
||||
Subject: "111",
|
||||
})
|
||||
|
||||
if !ret {
|
||||
t.Errorf("失败")
|
||||
}
|
||||
}
|
81
server/services/rule/rule.go
Normal file
81
server/services/rule/rule.go
Normal file
@ -0,0 +1,81 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cast"
|
||||
"pmail/config"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/models"
|
||||
"pmail/services/rule/match"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/send"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetAllRules(ctx *context.Context) []*dto.Rule {
|
||||
var res []*models.Rule
|
||||
var err error
|
||||
if ctx == nil {
|
||||
err = db.Instance.Select(&res, "select * from rule order by sort desc")
|
||||
} else {
|
||||
err = db.Instance.Select(&res, db.WithContext(ctx, "select * from rule where user_id=? order by sort desc"), ctx.UserID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("sqlERror :%v", err)
|
||||
}
|
||||
var ret []*dto.Rule
|
||||
for _, rule := range res {
|
||||
ret = append(ret, (&dto.Rule{}).Decode(rule))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func MatchRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) bool {
|
||||
|
||||
for _, r := range rule.Rules {
|
||||
var m match.Match
|
||||
|
||||
switch r.Type {
|
||||
case match.RuleTypeRegex:
|
||||
m = match.NewRegexMatch(r.Field, r.Rule)
|
||||
case match.RuleTypeContains:
|
||||
m = match.NewContainsMatch(r.Field, r.Rule)
|
||||
case match.RuleTypeEq:
|
||||
m = match.NewEqualMatch(r.Field, r.Rule)
|
||||
}
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !m.Match(ctx, email) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func DoRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) {
|
||||
switch rule.Action {
|
||||
case dto.READ:
|
||||
email.IsRead = 1
|
||||
case dto.DELETE:
|
||||
email.Status = 3
|
||||
case dto.FORWARD:
|
||||
if strings.Contains(rule.Params, config.Instance.Domain) {
|
||||
log.WithContext(ctx).Errorf("Forward Error! loop forwarding!")
|
||||
return
|
||||
}
|
||||
|
||||
err := send.Forward(nil, email, rule.Params)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("Forward Error:%v", err)
|
||||
}
|
||||
case dto.MOVE:
|
||||
email.GroupId = cast.ToInt(rule.Params)
|
||||
}
|
||||
}
|
@ -5,15 +5,15 @@ import (
|
||||
"os"
|
||||
"pmail/config"
|
||||
"pmail/db"
|
||||
"pmail/dto"
|
||||
"pmail/models"
|
||||
"pmail/utils/array"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/errors"
|
||||
"pmail/utils/file"
|
||||
"pmail/utils/password"
|
||||
)
|
||||
|
||||
func GetDatabaseSettings(ctx *dto.Context) (string, string, error) {
|
||||
func GetDatabaseSettings(ctx *context.Context) (string, string, error) {
|
||||
configData, err := ReadConfig()
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err)
|
||||
@ -26,7 +26,7 @@ func GetDatabaseSettings(ctx *dto.Context) (string, string, error) {
|
||||
return configData.DbType, configData.DbDSN, nil
|
||||
}
|
||||
|
||||
func GetAdminPassword(ctx *dto.Context) (string, error) {
|
||||
func GetAdminPassword(ctx *context.Context) (string, error) {
|
||||
|
||||
users := []*models.User{}
|
||||
err := db.Instance.Select(&users, "select * from user")
|
||||
@ -41,7 +41,7 @@ func GetAdminPassword(ctx *dto.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func SetAdminPassword(ctx *dto.Context, account, pwd string) error {
|
||||
func SetAdminPassword(ctx *context.Context, account, pwd string) error {
|
||||
encodePwd := password.Encode(pwd)
|
||||
res, err := db.Instance.Exec(db.WithContext(ctx, "INSERT INTO user (account, name, password) VALUES (?, 'admin',?)"), account, encodePwd)
|
||||
if err != nil {
|
||||
@ -58,7 +58,7 @@ func SetAdminPassword(ctx *dto.Context, account, pwd string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetDatabaseSettings(ctx *dto.Context, dbType, dbDSN string) error {
|
||||
func SetDatabaseSettings(ctx *context.Context, dbType, dbDSN string) error {
|
||||
configData, err := ReadConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
@ -68,6 +68,10 @@ func SetDatabaseSettings(ctx *dto.Context, dbType, dbDSN string) error {
|
||||
return errors.New("dbtype error")
|
||||
}
|
||||
|
||||
if dbDSN == "" {
|
||||
return errors.New("DSN error")
|
||||
}
|
||||
|
||||
configData.DbType = dbType
|
||||
configData.DbDSN = dbDSN
|
||||
|
||||
|
@ -5,9 +5,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"pmail/dto"
|
||||
"pmail/i18n"
|
||||
"pmail/services/auth"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/errors"
|
||||
)
|
||||
|
||||
@ -19,7 +19,7 @@ type DNSItem struct {
|
||||
Tips string `json:"tips"`
|
||||
}
|
||||
|
||||
func GetDNSSettings(ctx *dto.Context) ([]*DNSItem, error) {
|
||||
func GetDNSSettings(ctx *context.Context) ([]*DNSItem, error) {
|
||||
configData, err := ReadConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
|
@ -1,13 +1,13 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"pmail/dto"
|
||||
"pmail/signal"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/errors"
|
||||
)
|
||||
|
||||
// Finish 标记初始化完成
|
||||
func Finish(ctx *dto.Context) error {
|
||||
func Finish(ctx *context.Context) error {
|
||||
cfg, err := ReadConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
|
@ -14,7 +14,7 @@ var Instance *scs.SessionManager
|
||||
|
||||
func Init() {
|
||||
Instance = scs.New()
|
||||
Instance.Lifetime = 24 * time.Hour
|
||||
Instance.Lifetime = 7 * 24 * time.Hour
|
||||
// 使用db存储session数据,目前为了架构简单,
|
||||
// 暂不引入redis存储,如果日后性能存在瓶颈,可以将session迁移到redis
|
||||
if config.Instance.DbType == "mysql" {
|
||||
|
@ -8,9 +8,11 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"pmail/config"
|
||||
"pmail/db"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/hooks"
|
||||
"pmail/services/rule"
|
||||
"pmail/utils/async"
|
||||
"strings"
|
||||
"time"
|
||||
@ -57,6 +59,17 @@ func (s *Session) Data(r io.Reader) error {
|
||||
spfV = 1
|
||||
}
|
||||
|
||||
// 垃圾过滤
|
||||
if config.Instance.SpamFilterLevel == 1 && !SPFStatus && !dkimStatus {
|
||||
log.Infoln("垃圾邮件,拒信")
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.Instance.SpamFilterLevel == 2 && !SPFStatus {
|
||||
log.Infoln("垃圾邮件,拒信")
|
||||
return nil
|
||||
}
|
||||
|
||||
as2 := async.New(nil)
|
||||
for _, hook := range hooks.HookList {
|
||||
if hook == nil {
|
||||
@ -68,7 +81,19 @@ func (s *Session) Data(r io.Reader) error {
|
||||
}
|
||||
as2.Wait()
|
||||
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
// 执行邮件规则
|
||||
rs := rule.GetAllRules(nil)
|
||||
for _, r := range rs {
|
||||
if rule.MatchRule(nil, r, email) {
|
||||
rule.DoRule(nil, r, email)
|
||||
}
|
||||
}
|
||||
|
||||
if email == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
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,is_read,status,group_id) VALUES (?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
_, err = db.Instance.Exec(sql,
|
||||
email.Date,
|
||||
email.Subject,
|
||||
@ -84,7 +109,11 @@ func (s *Session) Data(r io.Reader) error {
|
||||
json2string(email.Attachments),
|
||||
spfV,
|
||||
dkimV,
|
||||
time.Now())
|
||||
time.Now(),
|
||||
email.IsRead,
|
||||
email.Status,
|
||||
email.GroupId,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Println("mysql insert error:", err.Error())
|
||||
|
@ -70,3 +70,352 @@ func TestSession_DataGmail(t *testing.T) {
|
||||
s.Data(bytes.NewReader(data))
|
||||
|
||||
}
|
||||
|
||||
func TestPmailEmail(t *testing.T) {
|
||||
testInit()
|
||||
emailData := `DKIM-Signature: a=rsa-sha256; bh=x7Rh+N2y2K9exccEAyKCTAGDgYKfnLZpMWc25ug5Ny4=;
|
||||
c=simple/simple; d=domain.com;
|
||||
h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693831868;
|
||||
v=1;
|
||||
b=1PZEupYvSMtGyYx42b4G65YbdnRj4y2QFo9kS7GXiTVhUM5EYzJhZzknwRMN5RL5aFY26W4E
|
||||
DmzJ85XvPPvrDtnU/B4jkc5xthE+KEsb1Go8HcL8WQqwvsE9brepeA0t0RiPnA/x7dbTo3u72SG
|
||||
WqtviWbJH5lPFc9PkSbEPFtc=
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=3c13260efb7bd8bad8315c21215489fe283f36cdf82813674f6e11215f6c
|
||||
Mime-Version: 1.0
|
||||
Subject: =?utf-8?q?=E6=8F=92=E4=BB=B6=E6=B5=8B=E8=AF=95?=
|
||||
To: =?utf-8?q?=E5=90=8D?= <ok@jinnrry.com>
|
||||
From: =?utf-8?q?=E5=8F=91=E9=80=81=E4=BA=BA?= <j@jinnrry.com>
|
||||
Date: Mon, 04 Sep 2023 20:51:08 +0800
|
||||
|
||||
--3c13260efb7bd8bad8315c21215489fe283f36cdf82813674f6e11215f6c
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=9ebf2f3c4f97c51dd9a285ae28a54d2d0d84aa6d0ad28b76547e2096bb66
|
||||
|
||||
--9ebf2f3c4f97c51dd9a285ae28a54d2d0d84aa6d0ad28b76547e2096bb66
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain
|
||||
|
||||
=E8=BF=99=E6=98=AFText
|
||||
--9ebf2f3c4f97c51dd9a285ae28a54d2d0d84aa6d0ad28b76547e2096bb66
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/html
|
||||
|
||||
<div>=E8=BF=99=E6=98=AFHtml</div>
|
||||
--9ebf2f3c4f97c51dd9a285ae28a54d2d0d84aa6d0ad28b76547e2096bb66--
|
||||
|
||||
--3c13260efb7bd8bad8315c21215489fe283f36cdf82813674f6e11215f6c--
|
||||
`
|
||||
s := Session{
|
||||
RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
|
||||
}
|
||||
|
||||
s.Data(bytes.NewReader([]byte(emailData)))
|
||||
|
||||
}
|
||||
|
||||
func TestRuleForward(t *testing.T) {
|
||||
testInit()
|
||||
|
||||
forwardEmail := `DKIM-Signature: a=rsa-sha256; bh=bpOshF+iimuqAQijVxqkH6gPpWf8A+Ih30/tMjgEgS0=;
|
||||
c=simple/simple; d=jinnrry.com;
|
||||
h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992640;
|
||||
v=1;
|
||||
b=XiOgYL9iGrkuYzXBAf7DSO0sRbFr6aPOE4VikmselNKEF1UTjMPdiqpeHyx/i6BOQlJWWZEC
|
||||
PzceHTDFIStcZE6a5Sc1nh8Fis+gRkrheBO/zK/P5P/euK+0Fj5+0T82keNTSCgo1ZtEIubaNR0
|
||||
JvkwJ2ZC9g8xV6Yiq+ZhRriT8lZ6zeI55PPEFJIzFgZ7xDshDgx5E7J1xRXQqcEMV1rgVq04d3c
|
||||
6wjU+LLtghmgtUToRp3ASn6DhVO+Bbc4QkmcQ/StQH3681+1GVMHvQSBhSSymSRA71SikE2u3a1
|
||||
JnvbOP9fThP7h+6oFEIRuF7MwDb3JWY5BXiFFKCkecdFg==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=8e9d5abb6bdac11b8d7d6e13280af1a87d12b904a59368d6e852b0a4ce3e
|
||||
Mime-Version: 1.0
|
||||
Subject: forward
|
||||
To: <t@jiangwei.one>
|
||||
From: "i" <i@jinnrry.com>
|
||||
Date: Wed, 06 Sep 2023 17:30:40 +0800
|
||||
|
||||
--8e9d5abb6bdac11b8d7d6e13280af1a87d12b904a59368d6e852b0a4ce3e
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=a62ae91c159ea22e8196d57d344626eb00d1ddfa9c5064a39b01588aa992
|
||||
|
||||
--a62ae91c159ea22e8196d57d344626eb00d1ddfa9c5064a39b01588aa992
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain
|
||||
|
||||
hello pls Forward the email.
|
||||
--a62ae91c159ea22e8196d57d344626eb00d1ddfa9c5064a39b01588aa992
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/html
|
||||
|
||||
<p>hello pls Forward the email.</p>
|
||||
--a62ae91c159ea22e8196d57d344626eb00d1ddfa9c5064a39b01588aa992--
|
||||
|
||||
--8e9d5abb6bdac11b8d7d6e13280af1a87d12b904a59368d6e852b0a4ce3e--`
|
||||
|
||||
readEmail := `DKIM-Signature: a=rsa-sha256; bh=JcCDj6edb1bAwRbcFZ63plFZOeB5AdGWLE/PQ2FQ1Tc=;
|
||||
c=simple/simple; d=jinnrry.com;
|
||||
h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992600;
|
||||
v=1;
|
||||
b=rwlqSkDFKYH42pA1jsajemaw+4YdeLHPeqV4mLQrRdihgma1VSvXl5CEOur/KuwQuUarr2cu
|
||||
SntWrHE6+RnDaQcPEHbkgoMjEJw5+VPwkIvE6VSlMIB7jg93mGzvN2yjheWTePZ+cVPjOaIrgir
|
||||
wiT24hkrTHp+ONT8XoS0sDuY+ieyBZp/GCv/YvgE4t0JEkNozMAVWotrXxaICDzZoWP3NNmKLqg
|
||||
6He6zwWAl51r3W5R5weGBi6A/FqlHgHZGroXnNi+wolDuN6pQiVAJ7MZ6hboPCbCCRrBQDTdor5
|
||||
wEI2+MwlJ/d2f17wxoGmluCewbeYttuVcpUOVwACJKw3g==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3
|
||||
Mime-Version: 1.0
|
||||
Subject: read
|
||||
To: <t@jiangwei.one>
|
||||
From: "i" <i@jinnrry.com>
|
||||
Date: Wed, 06 Sep 2023 17:30:00 +0800
|
||||
|
||||
--9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
|
||||
|
||||
--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain
|
||||
|
||||
12 aRead 1sadf
|
||||
--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/html
|
||||
|
||||
<p>12 aRead 1sadf</p>
|
||||
--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884--
|
||||
|
||||
--9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3--`
|
||||
|
||||
moveEmail := `DKIM-Signature: a=rsa-sha256; bh=YQfG/wlHGhky6FNmpIwgDYDOc/uyivdBv+9S02Z04xY=;
|
||||
c=simple/simple; d=jinnrry.com;
|
||||
h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992542;
|
||||
v=1;
|
||||
b=IhxswOCq8I7CmCas1EMp+n8loR7illqlF0IJC6eN1+OLjI/E5BPzpP4HWkyqaAkd0Vn9i+Bn
|
||||
MVb5kNHZ2S7qt0rqAAc6Atc0i9WpLEI3Cng+VDn+difcMZlJSAkhLLn2sUsS4Fzqqo3Cbw62qSO
|
||||
TgnWRmlj9aM+5xfGcl/76WOvQQpahJbGg6Go51kFMeHVom/VeGKIgFBCeMe37T/LS03c3pAV8gA
|
||||
i6Zy3GYE57W/qU3oCzaGeS3n5zom/i74H4VipiVIMX/OBNYhdHWrP8vyjvzLFpJlXp6RvzcRl0P
|
||||
ytyiCZfE8G7fAFntp20LW70Y5Xgqqczk1jR578UDczVoA==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d
|
||||
Mime-Version: 1.0
|
||||
Subject: Move
|
||||
To: <t@jiangwei.one>
|
||||
From: "i" <i@jinnrry.com>
|
||||
Date: Wed, 06 Sep 2023 17:29:02 +0800
|
||||
|
||||
--c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
|
||||
|
||||
--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain
|
||||
|
||||
MOVE move Move
|
||||
--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/html
|
||||
|
||||
<p>MOVE move Move</p>
|
||||
--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966--
|
||||
|
||||
--c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d--`
|
||||
|
||||
deleteEmail := `DKIM-Signature: a=rsa-sha256; bh=dNtHGqd1NbRj0WSwrJmPsqAcAy3h/4kZK2HFQ0Asld8=;
|
||||
c=simple/simple; d=jinnrry.com;
|
||||
h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992495;
|
||||
v=1;
|
||||
b=QllU8lqGdoOMaGYp8d13oWytb7+RebqKjq4y8Rs/kOeQxoE8dSEVliK3eBiXidsNTdDtkTqf
|
||||
eiwjyRBK92NVCYprdJqLbu9qZ39BC2lk3NXttTSJ1+1ZZ/bGtIW5JIYn2pToED0MqVVkxGFUtl+
|
||||
qFmc4mWo5a4Mbij7xaAB3uJtHpBDt7q4Ovr2hiMetQv7YrhZvCt/xrH8Q9YzZ6xzFUL5ekW40eH
|
||||
oWElU1GyVBHWCKh31aweyhA+1XLPYojjREQYd4svRqTbSFSsBqFwFIUGdnyJh2WgmF8eucmttAw
|
||||
oRhgzyZkHL1jAskKFBpO10SDReyk50Cvc+0kSLj+QcUpg==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3
|
||||
Mime-Version: 1.0
|
||||
Subject: test
|
||||
To: <t@jiangwei.one>
|
||||
From: "i" <i@jinnrry.com>
|
||||
Date: Wed, 06 Sep 2023 17:28:15 +0800
|
||||
|
||||
--bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
|
||||
|
||||
--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain
|
||||
|
||||
Delete
|
||||
--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/html
|
||||
|
||||
<p>Delete</p>
|
||||
--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04--
|
||||
|
||||
--bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3--`
|
||||
|
||||
s := Session{
|
||||
RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
|
||||
}
|
||||
|
||||
s.Data(bytes.NewReader([]byte(deleteEmail)))
|
||||
s.Data(bytes.NewReader([]byte(readEmail)))
|
||||
s.Data(bytes.NewReader([]byte(forwardEmail)))
|
||||
s.Data(bytes.NewReader([]byte(moveEmail)))
|
||||
}
|
||||
|
||||
func TestRuleRead(t *testing.T) {
|
||||
testInit()
|
||||
|
||||
readEmail := `DKIM-Signature: a=rsa-sha256; bh=JcCDj6edb1bAwRbcFZ63plFZOeB5AdGWLE/PQ2FQ1Tc=;
|
||||
c=simple/simple; d=jinnrry.com;
|
||||
h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992600;
|
||||
v=1;
|
||||
b=rwlqSkDFKYH42pA1jsajemaw+4YdeLHPeqV4mLQrRdihgma1VSvXl5CEOur/KuwQuUarr2cu
|
||||
SntWrHE6+RnDaQcPEHbkgoMjEJw5+VPwkIvE6VSlMIB7jg93mGzvN2yjheWTePZ+cVPjOaIrgir
|
||||
wiT24hkrTHp+ONT8XoS0sDuY+ieyBZp/GCv/YvgE4t0JEkNozMAVWotrXxaICDzZoWP3NNmKLqg
|
||||
6He6zwWAl51r3W5R5weGBi6A/FqlHgHZGroXnNi+wolDuN6pQiVAJ7MZ6hboPCbCCRrBQDTdor5
|
||||
wEI2+MwlJ/d2f17wxoGmluCewbeYttuVcpUOVwACJKw3g==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3
|
||||
Mime-Version: 1.0
|
||||
Subject: read
|
||||
To: <t@jiangwei.one>
|
||||
From: "i" <i@jinnrry.com>
|
||||
Date: Wed, 06 Sep 2023 17:30:00 +0800
|
||||
|
||||
--9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
|
||||
|
||||
--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain
|
||||
|
||||
12 aRead 1sadf
|
||||
--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/html
|
||||
|
||||
<p>12 aRead 1sadf</p>
|
||||
--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884--
|
||||
|
||||
--9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3--`
|
||||
|
||||
s := Session{
|
||||
RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
|
||||
}
|
||||
|
||||
s.Data(bytes.NewReader([]byte(readEmail)))
|
||||
|
||||
}
|
||||
|
||||
func TestRuleDelete(t *testing.T) {
|
||||
testInit()
|
||||
|
||||
deleteEmail := `DKIM-Signature: a=rsa-sha256; bh=dNtHGqd1NbRj0WSwrJmPsqAcAy3h/4kZK2HFQ0Asld8=;
|
||||
c=simple/simple; d=jinnrry.com;
|
||||
h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992495;
|
||||
v=1;
|
||||
b=QllU8lqGdoOMaGYp8d13oWytb7+RebqKjq4y8Rs/kOeQxoE8dSEVliK3eBiXidsNTdDtkTqf
|
||||
eiwjyRBK92NVCYprdJqLbu9qZ39BC2lk3NXttTSJ1+1ZZ/bGtIW5JIYn2pToED0MqVVkxGFUtl+
|
||||
qFmc4mWo5a4Mbij7xaAB3uJtHpBDt7q4Ovr2hiMetQv7YrhZvCt/xrH8Q9YzZ6xzFUL5ekW40eH
|
||||
oWElU1GyVBHWCKh31aweyhA+1XLPYojjREQYd4svRqTbSFSsBqFwFIUGdnyJh2WgmF8eucmttAw
|
||||
oRhgzyZkHL1jAskKFBpO10SDReyk50Cvc+0kSLj+QcUpg==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3
|
||||
Mime-Version: 1.0
|
||||
Subject: test
|
||||
To: <t@jiangwei.one>
|
||||
From: "i" <i@jinnrry.com>
|
||||
Date: Wed, 06 Sep 2023 17:28:15 +0800
|
||||
|
||||
--bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
|
||||
|
||||
--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain
|
||||
|
||||
Delete
|
||||
--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/html
|
||||
|
||||
<p>Delete</p>
|
||||
--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04--
|
||||
|
||||
--bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3--`
|
||||
|
||||
s := Session{
|
||||
RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
|
||||
}
|
||||
|
||||
s.Data(bytes.NewReader([]byte(deleteEmail)))
|
||||
|
||||
}
|
||||
|
||||
func TestRuleMove(t *testing.T) {
|
||||
testInit()
|
||||
|
||||
moveEmail := `DKIM-Signature: a=rsa-sha256; bh=YQfG/wlHGhky6FNmpIwgDYDOc/uyivdBv+9S02Z04xY=;
|
||||
c=simple/simple; d=jinnrry.com;
|
||||
h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992542;
|
||||
v=1;
|
||||
b=IhxswOCq8I7CmCas1EMp+n8loR7illqlF0IJC6eN1+OLjI/E5BPzpP4HWkyqaAkd0Vn9i+Bn
|
||||
MVb5kNHZ2S7qt0rqAAc6Atc0i9WpLEI3Cng+VDn+difcMZlJSAkhLLn2sUsS4Fzqqo3Cbw62qSO
|
||||
TgnWRmlj9aM+5xfGcl/76WOvQQpahJbGg6Go51kFMeHVom/VeGKIgFBCeMe37T/LS03c3pAV8gA
|
||||
i6Zy3GYE57W/qU3oCzaGeS3n5zom/i74H4VipiVIMX/OBNYhdHWrP8vyjvzLFpJlXp6RvzcRl0P
|
||||
ytyiCZfE8G7fAFntp20LW70Y5Xgqqczk1jR578UDczVoA==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d
|
||||
Mime-Version: 1.0
|
||||
Subject: Move
|
||||
To: <t@jiangwei.one>
|
||||
From: "i" <i@jinnrry.com>
|
||||
Date: Wed, 06 Sep 2023 17:29:02 +0800
|
||||
|
||||
--c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
|
||||
|
||||
--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain
|
||||
|
||||
MOVE move Move
|
||||
--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/html
|
||||
|
||||
<p>MOVE move Move</p>
|
||||
--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966--
|
||||
|
||||
--c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d--`
|
||||
|
||||
s := Session{
|
||||
RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
|
||||
}
|
||||
|
||||
s.Data(bytes.NewReader([]byte(moveEmail)))
|
||||
}
|
||||
|
@ -1,160 +0,0 @@
|
||||
package smtp_server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"pmail/dto"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/utils/array"
|
||||
"pmail/utils/async"
|
||||
"pmail/utils/smtp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type mxDomain struct {
|
||||
domain string
|
||||
mxHost string
|
||||
}
|
||||
|
||||
func Send(ctx *dto.Context, e *parsemail.Email) (error, map[string]error) {
|
||||
|
||||
b := e.BuildBytes(ctx)
|
||||
|
||||
var to []*parsemail.User
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
|
||||
// 按域名整理
|
||||
toByDomain := map[mxDomain][]*parsemail.User{}
|
||||
for _, s := range to {
|
||||
args := strings.Split(s.EmailAddress, "@")
|
||||
if len(args) == 2 {
|
||||
//查询dns mx记录
|
||||
mxInfo, err := net.LookupMX(args[1])
|
||||
address := mxDomain{
|
||||
domain: "smtp." + args[1],
|
||||
mxHost: "smtp." + args[1],
|
||||
}
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
|
||||
}
|
||||
if len(mxInfo) > 0 {
|
||||
address = mxDomain{
|
||||
domain: args[1],
|
||||
mxHost: mxInfo[0].Host,
|
||||
}
|
||||
}
|
||||
toByDomain[address] = append(toByDomain[address], s)
|
||||
} else {
|
||||
log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var errEmailAddress []string
|
||||
|
||||
errMap := map[string]error{}
|
||||
|
||||
as := async.New(ctx)
|
||||
for domain, tos := range toByDomain {
|
||||
domain := domain
|
||||
tos := tos
|
||||
as.WaitProcess(func(p any) {
|
||||
|
||||
err := smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
|
||||
// 重新选取证书域名
|
||||
if err != nil {
|
||||
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
|
||||
if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
|
||||
if hostnameErr.Certificate != nil {
|
||||
certificateHostName := hostnameErr.Certificate.DNSNames
|
||||
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
|
||||
for _, user := range tos {
|
||||
errEmailAddress = append(errEmailAddress, user.EmailAddress)
|
||||
|
||||
}
|
||||
}
|
||||
errMap[domain.domain] = err
|
||||
}, nil)
|
||||
}
|
||||
as.Wait()
|
||||
|
||||
if len(errEmailAddress) > 0 {
|
||||
return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ",")), errMap
|
||||
}
|
||||
return nil, errMap
|
||||
|
||||
}
|
||||
|
||||
func buildAddress(u []*parsemail.User) []string {
|
||||
var ret []string
|
||||
|
||||
for _, user := range u {
|
||||
ret = append(ret, user.EmailAddress)
|
||||
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func domainMatch(domain string, dnsNames []string) string {
|
||||
secondMatch := ""
|
||||
|
||||
for _, name := range dnsNames {
|
||||
if strings.Contains(name, "smtp") {
|
||||
secondMatch = name
|
||||
}
|
||||
|
||||
if name == domain {
|
||||
return name
|
||||
}
|
||||
if strings.Contains(name, "*") {
|
||||
nameArg := strings.Split(name, ".")
|
||||
domainArg := strings.Split(domain, ".")
|
||||
match := true
|
||||
for i := 0; i < len(nameArg); i++ {
|
||||
if nameArg[len(nameArg)-1-i] == "*" {
|
||||
continue
|
||||
}
|
||||
if len(domainArg) > i {
|
||||
if nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
match = false
|
||||
break
|
||||
}
|
||||
|
||||
for i := 0; i < len(domainArg); i++ {
|
||||
if len(nameArg) > i && nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
|
||||
continue
|
||||
}
|
||||
if len(nameArg) > i && nameArg[len(nameArg)-1-i] == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
match = false
|
||||
break
|
||||
}
|
||||
if match {
|
||||
return domain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if secondMatch != "" {
|
||||
return strings.ReplaceAll(secondMatch, "*.", "")
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(dnsNames[0], "*.", "")
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package smtp_server
|
||||
|
||||
import (
|
||||
"pmail/dto/parsemail"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSend(t *testing.T) {
|
||||
testInit()
|
||||
e := &parsemail.Email{
|
||||
From: &parsemail.User{
|
||||
Name: "发送人",
|
||||
EmailAddress: "j@jinnrry.com",
|
||||
},
|
||||
To: []*parsemail.User{
|
||||
{"ok@jinnrry.com", "名"},
|
||||
{"ok@xjiangwei.cn", "字"},
|
||||
},
|
||||
Subject: "你好",
|
||||
Text: []byte("这是Text"),
|
||||
HTML: []byte("<div>这是Html</div>"),
|
||||
}
|
||||
Send(nil, e)
|
||||
}
|
12
server/utils/address/address.go
Normal file
12
server/utils/address/address.go
Normal file
@ -0,0 +1,12 @@
|
||||
package address
|
||||
|
||||
import "strings"
|
||||
|
||||
// IsValidEmailAddress 检查是否是有效的邮箱地址
|
||||
func IsValidEmailAddress(str string) bool {
|
||||
ars := strings.Split(str, "@")
|
||||
if len(ars) != 2 {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(ars[1], ".")
|
||||
}
|
42
server/utils/address/address_test.go
Normal file
42
server/utils/address/address_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package address
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsValidEmailAddress(t *testing.T) {
|
||||
type args struct {
|
||||
str string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"",
|
||||
args{"test@qq.com"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"",
|
||||
args{"1000@qq.com"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"",
|
||||
args{"1000@163.com"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"",
|
||||
args{"1000@1631com"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValidEmailAddress(tt.args.str); got != tt.want {
|
||||
t.Errorf("IsValidEmailAddress() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cast"
|
||||
"pmail/dto"
|
||||
"pmail/utils/context"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
)
|
||||
@ -14,10 +14,10 @@ type Callback func(params any)
|
||||
type Async struct {
|
||||
wg *sync.WaitGroup
|
||||
lastError error
|
||||
ctx *dto.Context
|
||||
ctx *context.Context
|
||||
}
|
||||
|
||||
func New(ctx *dto.Context) *Async {
|
||||
func New(ctx *context.Context) *Async {
|
||||
return &Async{
|
||||
ctx: ctx,
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package dto
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"pmail/models"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -11,9 +10,11 @@ const (
|
||||
|
||||
type Context struct {
|
||||
context.Context
|
||||
UserInfo *models.User
|
||||
values map[string]any
|
||||
Lang string
|
||||
UserID int
|
||||
UserAccount string
|
||||
UserName string
|
||||
values map[string]any
|
||||
Lang string
|
||||
}
|
||||
|
||||
func (c *Context) SetValue(key string, value any) {
|
260
server/utils/send/send.go
Normal file
260
server/utils/send/send.go
Normal file
@ -0,0 +1,260 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"pmail/dto/parsemail"
|
||||
"pmail/utils/array"
|
||||
"pmail/utils/async"
|
||||
"pmail/utils/context"
|
||||
"pmail/utils/smtp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type mxDomain struct {
|
||||
domain string
|
||||
mxHost string
|
||||
}
|
||||
|
||||
// Forward 转发邮件
|
||||
func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string) error {
|
||||
|
||||
b := e.ForwardBuildBytes(ctx, forwardAddress)
|
||||
|
||||
var to []*parsemail.User
|
||||
to = []*parsemail.User{
|
||||
{EmailAddress: forwardAddress},
|
||||
}
|
||||
|
||||
// 按域名整理
|
||||
toByDomain := map[mxDomain][]*parsemail.User{}
|
||||
for _, s := range to {
|
||||
args := strings.Split(s.EmailAddress, "@")
|
||||
if len(args) == 2 {
|
||||
//查询dns mx记录
|
||||
mxInfo, err := net.LookupMX(args[1])
|
||||
address := mxDomain{
|
||||
domain: "smtp." + args[1],
|
||||
mxHost: "smtp." + args[1],
|
||||
}
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
|
||||
}
|
||||
if len(mxInfo) > 0 {
|
||||
address = mxDomain{
|
||||
domain: args[1],
|
||||
mxHost: mxInfo[0].Host,
|
||||
}
|
||||
}
|
||||
toByDomain[address] = append(toByDomain[address], s)
|
||||
} else {
|
||||
log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var errEmailAddress []string
|
||||
|
||||
errMap := map[string]error{}
|
||||
|
||||
as := async.New(ctx)
|
||||
for domain, tos := range toByDomain {
|
||||
domain := domain
|
||||
tos := tos
|
||||
as.WaitProcess(func(p any) {
|
||||
|
||||
// 先使用smtps协议尝试
|
||||
err := smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
if err != nil {
|
||||
// smtps发送失败,尝试smtp
|
||||
err = smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
}
|
||||
|
||||
// 重新选取证书域名
|
||||
if err != nil {
|
||||
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
|
||||
if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
|
||||
if hostnameErr.Certificate != nil {
|
||||
certificateHostName := hostnameErr.Certificate.DNSNames
|
||||
// 先使用smtps协议尝试
|
||||
err = smtp.SendMailWithTls(domainMatch(domain.domain, certificateHostName), domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
if err != nil {
|
||||
log.Infoln(err)
|
||||
// smtps发送失败,尝试smtp
|
||||
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
|
||||
for _, user := range tos {
|
||||
errEmailAddress = append(errEmailAddress, user.EmailAddress)
|
||||
|
||||
}
|
||||
}
|
||||
errMap[domain.domain] = err
|
||||
}, nil)
|
||||
}
|
||||
as.Wait()
|
||||
|
||||
if len(errEmailAddress) > 0 {
|
||||
return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ","))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
|
||||
|
||||
b := e.BuildBytes(ctx)
|
||||
|
||||
var to []*parsemail.User
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
|
||||
// 按域名整理
|
||||
toByDomain := map[mxDomain][]*parsemail.User{}
|
||||
for _, s := range to {
|
||||
args := strings.Split(s.EmailAddress, "@")
|
||||
if len(args) == 2 {
|
||||
//查询dns mx记录
|
||||
mxInfo, err := net.LookupMX(args[1])
|
||||
address := mxDomain{
|
||||
domain: "smtp." + args[1],
|
||||
mxHost: "smtp." + args[1],
|
||||
}
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
|
||||
}
|
||||
if len(mxInfo) > 0 {
|
||||
address = mxDomain{
|
||||
domain: args[1],
|
||||
mxHost: mxInfo[0].Host,
|
||||
}
|
||||
}
|
||||
toByDomain[address] = append(toByDomain[address], s)
|
||||
} else {
|
||||
log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var errEmailAddress []string
|
||||
|
||||
errMap := map[string]error{}
|
||||
|
||||
as := async.New(ctx)
|
||||
for domain, tos := range toByDomain {
|
||||
domain := domain
|
||||
tos := tos
|
||||
as.WaitProcess(func(p any) {
|
||||
|
||||
// 先使用smtps协议尝试
|
||||
err := smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
if err != nil {
|
||||
// smtps发送失败,尝试smtp
|
||||
err = smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
}
|
||||
|
||||
// 重新选取证书域名
|
||||
if err != nil {
|
||||
if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
|
||||
if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
|
||||
if hostnameErr.Certificate != nil {
|
||||
certificateHostName := hostnameErr.Certificate.DNSNames
|
||||
// 先使用smtps协议尝试
|
||||
err = smtp.SendMailWithTls(domainMatch(domain.domain, certificateHostName), domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
if err != nil {
|
||||
// smtps发送失败,尝试smtp
|
||||
err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
|
||||
for _, user := range tos {
|
||||
errEmailAddress = append(errEmailAddress, user.EmailAddress)
|
||||
|
||||
}
|
||||
}
|
||||
errMap[domain.domain] = err
|
||||
}, nil)
|
||||
}
|
||||
as.Wait()
|
||||
|
||||
if len(errEmailAddress) > 0 {
|
||||
return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ",")), errMap
|
||||
}
|
||||
return nil, errMap
|
||||
|
||||
}
|
||||
|
||||
func buildAddress(u []*parsemail.User) []string {
|
||||
var ret []string
|
||||
|
||||
for _, user := range u {
|
||||
ret = append(ret, user.EmailAddress)
|
||||
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func domainMatch(domain string, dnsNames []string) string {
|
||||
secondMatch := ""
|
||||
|
||||
for _, name := range dnsNames {
|
||||
if strings.Contains(name, "smtp") {
|
||||
secondMatch = name
|
||||
}
|
||||
|
||||
if name == domain {
|
||||
return name
|
||||
}
|
||||
if strings.Contains(name, "*") {
|
||||
nameArg := strings.Split(name, ".")
|
||||
domainArg := strings.Split(domain, ".")
|
||||
match := true
|
||||
for i := 0; i < len(nameArg); i++ {
|
||||
if nameArg[len(nameArg)-1-i] == "*" {
|
||||
continue
|
||||
}
|
||||
if len(domainArg) > i {
|
||||
if nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
match = false
|
||||
break
|
||||
}
|
||||
|
||||
for i := 0; i < len(domainArg); i++ {
|
||||
if len(nameArg) > i && nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
|
||||
continue
|
||||
}
|
||||
if len(nameArg) > i && nameArg[len(nameArg)-1-i] == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
match = false
|
||||
break
|
||||
}
|
||||
if match {
|
||||
return domain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if secondMatch != "" {
|
||||
return strings.ReplaceAll(secondMatch, "*.", "")
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(dnsNames[0], "*.", "")
|
||||
}
|
51
server/utils/send/send_test.go
Normal file
51
server/utils/send/send_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"pmail/config"
|
||||
"pmail/dto/parsemail"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testInit() {
|
||||
// 设置日志格式为json格式
|
||||
//log.SetFormatter(&log.JSONFormatter{})
|
||||
|
||||
log.SetReportCaller(true)
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
//以下设置只是为了使输出更美观
|
||||
DisableColors: true,
|
||||
TimestampFormat: "2006-01-02 15:03:04",
|
||||
})
|
||||
|
||||
// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
|
||||
// 日志消息输出可以是任意的io.writer类型
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
// 设置日志级别为warn以上
|
||||
log.SetLevel(log.TraceLevel)
|
||||
|
||||
var cst, _ = time.LoadLocation("Asia/Shanghai")
|
||||
time.Local = cst
|
||||
|
||||
config.Init()
|
||||
parsemail.Init()
|
||||
}
|
||||
func TestSend(t *testing.T) {
|
||||
testInit()
|
||||
e := &parsemail.Email{
|
||||
From: &parsemail.User{
|
||||
Name: "发送人",
|
||||
EmailAddress: "j@jinnrry.com",
|
||||
},
|
||||
To: []*parsemail.User{
|
||||
{"ok@jinnrry.com", "名"},
|
||||
},
|
||||
Subject: "插件测试",
|
||||
Text: []byte("这是Text"),
|
||||
HTML: []byte("<div>这是Html</div>"),
|
||||
}
|
||||
Send(nil, e)
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
// Some external packages provide more functionality. See:
|
||||
//
|
||||
// https://godoc.org/?q=smtp
|
||||
//
|
||||
// 在go原始SMTP协议的基础上修复了TLS验证错误、支持了SMTPS协议
|
||||
package smtp
|
||||
|
||||
import (
|
||||
@ -26,7 +28,6 @@ import (
|
||||
"net"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"pmail/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -61,6 +62,22 @@ func Dial(addr string) (*Client, error) {
|
||||
return NewClient(conn, host)
|
||||
}
|
||||
|
||||
// with tls
|
||||
func DialTls(addr, domain string) (*Client, error) {
|
||||
// TLS config
|
||||
tlsconfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: domain,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(addr)
|
||||
return NewClient(conn, host)
|
||||
}
|
||||
|
||||
// NewClient returns a new Client using an existing connection and host as a
|
||||
// server name to be used when authenticating.
|
||||
func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||
@ -70,7 +87,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||
text.Close()
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{Text: text, conn: conn, serverName: host, localName: config.Instance.Domain}
|
||||
c := &Client{Text: text, conn: conn, serverName: host, localName: "jinnrry.com"}
|
||||
_, c.tls = conn.(*tls.Conn)
|
||||
return c, nil
|
||||
}
|
||||
@ -306,7 +323,53 @@ func (c *Client) Data() (io.WriteCloser, error) {
|
||||
return &dataCloser{c, c.Text.DotWriter()}, nil
|
||||
}
|
||||
|
||||
var testHookStartTLS func(*tls.Config) // nil, except for tests
|
||||
func SendMailWithTls(domain string, addr string, a smtp.Auth, from string, to []string, msg []byte) error {
|
||||
if err := validateLine(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, recp := range to {
|
||||
if err := validateLine(recp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c, err := DialTls(addr, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
if a != nil && c.ext != nil {
|
||||
if _, ok := c.ext["AUTH"]; !ok {
|
||||
return errors.New("smtp: server doesn't support AUTH")
|
||||
}
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// SendMail connects to the server at addr, switches to TLS if
|
||||
// possible, authenticates with the optional mechanism a if possible,
|
||||
|
Loading…
x
Reference in New Issue
Block a user