mirror of https://github.com/certd/certd
perf: email proxy
parent
14ab93dc2f
commit
453f1baa0b
|
@ -28,6 +28,7 @@ class LicenseHolder {
|
|||
expireTime = 0;
|
||||
level = 1;
|
||||
message?: string = undefined;
|
||||
secret?: string = undefined;
|
||||
}
|
||||
const holder = new LicenseHolder();
|
||||
holder.isPlus = false;
|
||||
|
@ -44,12 +45,14 @@ class LicenseVerifier {
|
|||
if (value && info) {
|
||||
holder.isPlus = true;
|
||||
holder.expireTime = info.expireTime;
|
||||
holder.secret = info.secret;
|
||||
holder.level = info.level;
|
||||
} else {
|
||||
holder.isPlus = false;
|
||||
holder.expireTime = 0;
|
||||
holder.level = 1;
|
||||
holder.message = info.message;
|
||||
holder.secret = undefined;
|
||||
}
|
||||
return {
|
||||
...holder,
|
||||
|
@ -87,6 +90,7 @@ class LicenseVerifier {
|
|||
return this.setPlus(true, {
|
||||
expireTime: json.expireTime,
|
||||
level: json.level || 1,
|
||||
secret: json.secret,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -108,6 +112,7 @@ export function getPlusInfo() {
|
|||
isPlus: holder.isPlus,
|
||||
level: holder.level,
|
||||
expireTime: holder.expireTime,
|
||||
secret: holder.secret,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,23 @@ export function createAxiosService({ logger }: { logger: Logger }) {
|
|||
logger.error(`请求出错:url:${error?.response?.config.url},method:${error?.response?.config?.method},status:${error?.response?.status}`);
|
||||
logger.info("返回数据:", JSON.stringify(error?.response?.data));
|
||||
delete error.config;
|
||||
return Promise.reject(error.response || error);
|
||||
const data = error?.response?.data;
|
||||
if (!data) {
|
||||
error.message = data.message || data.msg || data.error || data;
|
||||
}
|
||||
if (error?.response) {
|
||||
return Promise.reject({
|
||||
status: error?.response?.status,
|
||||
statusText: error?.response?.statusText,
|
||||
request: {
|
||||
url: error?.response?.config?.url,
|
||||
method: error?.response?.config?.method,
|
||||
data: error?.response?.data,
|
||||
},
|
||||
data: error?.response?.data,
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
return service;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div id="userLayout" :class="['user-layout-wrapper']">
|
||||
<div class="login-container flex-center">
|
||||
<div class="user-layout-lang"></div>
|
||||
<div class="user-layout-content">
|
||||
<div class="top flex flex-col items-center justify-center">
|
||||
<div class="header flex flex-row items-center">
|
||||
|
|
|
@ -47,6 +47,9 @@ export const useUserStore = defineStore({
|
|||
},
|
||||
isAdmin(): boolean {
|
||||
return this.getUserInfo?.id === 1;
|
||||
},
|
||||
isPlus(): boolean {
|
||||
return this.plusInfo?.isPlus || false;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
|
|
@ -4,32 +4,47 @@
|
|||
<div class="title">邮件设置</div>
|
||||
</template>
|
||||
<div class="email-form">
|
||||
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
|
||||
<a-form-item label="SMTP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
|
||||
<a-input v-model:value="formState.host" />
|
||||
<a-form
|
||||
:model="formState"
|
||||
name="basic"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
autocomplete="off"
|
||||
@finish="onFinish"
|
||||
@finish-failed="onFinishFailed"
|
||||
>
|
||||
<a-form-item label="使用邮件代理" name="usePlus">
|
||||
<a-switch v-model:checked="formState.usePlus" :disabled="!userStore.isPlus" />
|
||||
<div class="helper">专业版功能,免除繁琐的邮件配置,直接发邮件</div>
|
||||
</a-form-item>
|
||||
<template v-if="!formState.usePlus">
|
||||
<a-form-item label="SMTP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
|
||||
<a-input v-model:value="formState.host" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="SMTP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
|
||||
<a-input v-model:value="formState.port" />
|
||||
</a-form-item>
|
||||
<a-form-item label="SMTP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
|
||||
<a-input v-model:value="formState.port" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]">
|
||||
<a-input v-model:value="formState.auth.user" />
|
||||
</a-form-item>
|
||||
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]">
|
||||
<a-input-password v-model:value="formState.auth.pass" />
|
||||
<div class="helper">如果是qq邮箱,需要到qq邮箱的设置里面申请授权码作为密码</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]">
|
||||
<a-input v-model:value="formState.sender" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否ssl" name="secure">
|
||||
<a-switch v-model:checked="formState.secure" />
|
||||
<div class="helper">ssl和非ssl的smtp端口是不一样的,注意修改端口</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
|
||||
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]">
|
||||
<a-input v-model:value="formState.auth.user" />
|
||||
</a-form-item>
|
||||
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]">
|
||||
<a-input-password v-model:value="formState.auth.pass" />
|
||||
<div class="helper">如果是qq邮箱,需要到qq邮箱的设置里面申请授权码作为密码</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]">
|
||||
<a-input v-model:value="formState.sender" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否ssl" name="secure">
|
||||
<a-switch v-model:checked="formState.secure" />
|
||||
<div class="helper">ssl和非ssl的smtp端口是不一样的,注意修改端口</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
|
||||
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
||||
<a-button type="primary" html-type="submit">保存</a-button>
|
||||
</a-form-item>
|
||||
|
@ -55,6 +70,7 @@ import * as emailApi from "./api.email";
|
|||
|
||||
import { SettingKeys } from "./api";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { useUserStore } from "/@/store/modules/user";
|
||||
|
||||
interface FormState {
|
||||
host: string;
|
||||
|
@ -69,6 +85,7 @@ interface FormState {
|
|||
rejectUnauthorized?: boolean;
|
||||
};
|
||||
sender: string;
|
||||
usePlus: boolean;
|
||||
}
|
||||
|
||||
const formState = reactive<Partial<FormState>>({
|
||||
|
@ -76,7 +93,8 @@ const formState = reactive<Partial<FormState>>({
|
|||
user: "",
|
||||
pass: ""
|
||||
},
|
||||
tls: {}
|
||||
tls: {},
|
||||
usePlus: false
|
||||
});
|
||||
|
||||
async function load() {
|
||||
|
@ -118,6 +136,8 @@ async function onTestSend() {
|
|||
testFormState.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
@ -127,9 +147,9 @@ async function onTestSend() {
|
|||
margin: 20px;
|
||||
}
|
||||
|
||||
.helper{
|
||||
padding:1px;
|
||||
margin:0px;
|
||||
.helper {
|
||||
padding: 1px;
|
||||
margin: 0px;
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import type { EmailSend } from '@certd/pipeline';
|
||||
import { IEmailService } from '@certd/pipeline';
|
||||
import { IEmailService, isPlus } from '@certd/pipeline';
|
||||
import nodemailer from 'nodemailer';
|
||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
import { logger } from '../../../utils/logger.js';
|
||||
import { UserSettingsService } from '../../mine/service/user-settings-service.js';
|
||||
import { PlusService } from './plus-service.js';
|
||||
|
||||
export type EmailConfig = {
|
||||
host: string;
|
||||
|
@ -19,26 +20,57 @@ export type EmailConfig = {
|
|||
rejectUnauthorized: boolean;
|
||||
};
|
||||
sender: string;
|
||||
usePlus?: boolean;
|
||||
} & SMTPConnection.Options;
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class EmailService implements IEmailService {
|
||||
@Inject()
|
||||
settingsService: UserSettingsService;
|
||||
@Inject()
|
||||
plusService: PlusService;
|
||||
|
||||
async sendByPlus(email: EmailSend) {
|
||||
if (!isPlus()) {
|
||||
throw new Error('plus not enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* userId: number;
|
||||
* subject: string;
|
||||
* content: string;
|
||||
* receivers: string[];
|
||||
*/
|
||||
|
||||
await this.plusService.request('/activation/emailSend', {
|
||||
subject: email.subject,
|
||||
text: email.content,
|
||||
to: email.receivers,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
async send(email: EmailSend) {
|
||||
console.log('sendEmail', email);
|
||||
|
||||
const emailConfigEntity = await this.settingsService.getByKey(
|
||||
'email',
|
||||
email.userId
|
||||
);
|
||||
const emailConfigEntity = await this.settingsService.getByKey('email', email.userId);
|
||||
if (emailConfigEntity == null || !emailConfigEntity.setting) {
|
||||
if (isPlus()) {
|
||||
//自动使用plus发邮件
|
||||
return await this.sendByPlus(email);
|
||||
}
|
||||
throw new Error('email settings 未设置');
|
||||
}
|
||||
const emailConfig = JSON.parse(emailConfigEntity.setting) as EmailConfig;
|
||||
if (emailConfig.usePlus && isPlus()) {
|
||||
return await this.sendByPlus(email);
|
||||
}
|
||||
await this.sendByCustom(emailConfig, email);
|
||||
logger.info('sendEmail complete: ', email);
|
||||
}
|
||||
|
||||
private async sendByCustom(emailConfig: EmailConfig, email: EmailSend) {
|
||||
const transporter = nodemailer.createTransport(emailConfig);
|
||||
const mailOptions = {
|
||||
from: emailConfig.sender,
|
||||
|
@ -47,7 +79,6 @@ export class EmailService implements IEmailService {
|
|||
text: email.content,
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
logger.info('sendEmail complete: ', email);
|
||||
}
|
||||
|
||||
async test(userId: number, receiver: string) {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { SysSettingsService } from '../../system/service/sys-settings-service.js';
|
||||
import { SysInstallInfo } from '../../system/service/models.js';
|
||||
import { appKey, getPlusInfo } from '@certd/pipeline';
|
||||
import * as crypto from 'crypto';
|
||||
import { request } from '../../../utils/http.js';
|
||||
import { logger } from '../../../utils/logger.js';
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class PlusService {
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
async request(url: string, data: any) {
|
||||
const timestamps = Date.now();
|
||||
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
|
||||
const sign = await this.sign(data, timestamps);
|
||||
|
||||
const requestHeader = {
|
||||
subjectId: installInfo.siteId,
|
||||
appKey: appKey,
|
||||
sign: sign,
|
||||
timestamps: timestamps,
|
||||
};
|
||||
let requestHeaderStr = JSON.stringify(requestHeader);
|
||||
requestHeaderStr = Buffer.from(requestHeaderStr).toString('base64');
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Plus-Subject': requestHeaderStr,
|
||||
};
|
||||
const baseUrl = 'http://127.0.0.1:11007';
|
||||
return await request({
|
||||
url: url,
|
||||
baseURL: baseUrl,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
headers: headers,
|
||||
});
|
||||
}
|
||||
|
||||
async sign(body: any, timestamps: number) {
|
||||
//content := fmt.Sprintf("%s.%d.%s", in.Params, in.Timestamps, secret)
|
||||
const params = JSON.stringify(body);
|
||||
const plusInfo = getPlusInfo();
|
||||
const secret = plusInfo.secret;
|
||||
const content = `${params}.${timestamps}.${secret}`;
|
||||
|
||||
// sha256
|
||||
const sign = crypto.createHash('sha256').update(content).digest('base64');
|
||||
logger.info('content:', content, 'sign:', sign);
|
||||
return sign;
|
||||
}
|
||||
}
|
|
@ -8,6 +8,9 @@ export async function request(config: any) {
|
|||
if (data) {
|
||||
throw new Error(data.message || data.msg || data.error || data);
|
||||
}
|
||||
if (e.statusText) {
|
||||
throw new Error(`请求失败:${e.request?.url} ${e.status} ${e.statusText}`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue