新增支持s3协议对象存储
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<meta name="theme-color" content="#D3E3FD" id="theme-color-meta">
|
<meta name="theme-color" content="#D3E3FD" id="theme-color-meta">
|
||||||
<title></title>
|
<title></title>
|
||||||
<link rel="icon" href="./src/assets/favicon.svg" type="image/svg+xml">
|
<link rel="icon" href="./src/assets/favicon.png" type="image/png">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
||||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 14 KiB |
BIN
mail-vue/src/assets/favicon.png
Normal file
BIN
mail-vue/src/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><g fill="none"><path fill="#367af2" d="M4 15.42v18.33A6.25 6.25 0 0 0 10.25 40h27.5A6.25 6.25 0 0 0 44 33.75V15.5L24.582 25.724a1.25 1.25 0 0 1-1.168-.002z"/><path fill="url(#SVG9uDrcZUx)" d="M4 15.42v18.33A6.25 6.25 0 0 0 10.25 40h27.5A6.25 6.25 0 0 0 44 33.75V15.5L24.582 25.724a1.25 1.25 0 0 1-1.168-.002z"/><path fill="url(#SVGkPPLtdEu)" d="M4 15.42v18.33A6.25 6.25 0 0 0 10.25 40h27.5A6.25 6.25 0 0 0 44 33.75V15.5L24.582 25.724a1.25 1.25 0 0 1-1.168-.002z"/><path fill="url(#SVG1EOghF8v)" fill-opacity="0.75" d="M4 15.42v18.33A6.25 6.25 0 0 0 10.25 40h27.5A6.25 6.25 0 0 0 44 33.75V15.5L24.582 25.724a1.25 1.25 0 0 1-1.168-.002z"/><path fill="url(#SVG72PJueUR)" fill-opacity="0.7" d="M4 15.42v18.33A6.25 6.25 0 0 0 10.25 40h27.5A6.25 6.25 0 0 0 44 33.75V15.5L24.582 25.724a1.25 1.25 0 0 1-1.168-.002z"/><path fill="url(#SVGCdktzmgW)" d="M4.02 13.747A6.25 6.25 0 0 1 10.25 8h27.5A6.25 6.25 0 0 1 44 14.25v1.38L24.582 25.854a1.25 1.25 0 0 1-1.168-.002L4 15.551V14.25q0-.254.02-.503"/><defs><linearGradient id="SVG9uDrcZUx" x1="31" x2="39.662" y1="19.5" y2="40.944" gradientUnits="userSpaceOnUse"><stop offset=".199" stop-color="#0094f0" stop-opacity="0"/><stop offset=".431" stop-color="#0094f0"/></linearGradient><linearGradient id="SVGkPPLtdEu" x1="18.286" x2="7.955" y1="18.008" y2="41.831" gradientUnits="userSpaceOnUse"><stop offset=".191" stop-color="#0094f0" stop-opacity="0"/><stop offset=".431" stop-color="#0094f0"/></linearGradient><linearGradient id="SVG1EOghF8v" x1="34.547" x2="36.228" y1="30.084" y2="42.272" gradientUnits="userSpaceOnUse"><stop stop-color="#2764e7" stop-opacity="0"/><stop offset="1" stop-color="#2764e7"/></linearGradient><linearGradient id="SVG72PJueUR" x1="30.191" x2="33.258" y1="18.439" y2="43.244" gradientUnits="userSpaceOnUse"><stop offset=".533" stop-color="#0094f0" stop-opacity="0"/><stop offset="1" stop-color="#0094f0"/></linearGradient><linearGradient id="SVGCdktzmgW" x1="15.883" x2="28.057" y1="2.275" y2="34.261" gradientUnits="userSpaceOnUse"><stop stop-color="#6ce0ff"/><stop offset=".462" stop-color="#29c3ff"/><stop offset="1" stop-color="#4894fe"/></linearGradient></defs></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -55,7 +55,9 @@ http.interceptors.response.use((res) => {
|
|||||||
})
|
})
|
||||||
reject(data)
|
reject(data)
|
||||||
}
|
}
|
||||||
resolve(data.data)
|
setTimeout(() => {
|
||||||
|
resolve(data.data)
|
||||||
|
},300)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|||||||
@@ -136,8 +136,6 @@ const en = {
|
|||||||
loginDomain: 'Sign-In Box Domain',
|
loginDomain: 'Sign-In Box Domain',
|
||||||
multipleEmail: 'Multiple Accounts',
|
multipleEmail: 'Multiple Accounts',
|
||||||
multipleEmailDesc: 'Enable this feature to allow users to add multiple accounts.',
|
multipleEmailDesc: 'Enable this feature to allow users to add multiple accounts.',
|
||||||
physicallyWipeData: 'Physically Wipe Data',
|
|
||||||
physicallyWipeDataDesc: 'This action will permanently erase all deleted data.',
|
|
||||||
customization: 'Customization',
|
customization: 'Customization',
|
||||||
websiteTitle: 'Title',
|
websiteTitle: 'Title',
|
||||||
loginBoxOpacity: 'Login Box Opacity',
|
loginBoxOpacity: 'Login Box Opacity',
|
||||||
@@ -148,7 +146,7 @@ const en = {
|
|||||||
autoRefreshDesc: 'Automatically fetch the latest emails from the server.',
|
autoRefreshDesc: 'Automatically fetch the latest emails from the server.',
|
||||||
sendEmail: 'Send Email',
|
sendEmail: 'Send Email',
|
||||||
resendToken: 'Resend Token',
|
resendToken: 'Resend Token',
|
||||||
R2OS: 'R2 Object Storage',
|
oss: 'Object Storage',
|
||||||
osDomain: 'Domain',
|
osDomain: 'Domain',
|
||||||
emailPush: 'Email Push',
|
emailPush: 'Email Push',
|
||||||
tgBot: 'Telegram Bot',
|
tgBot: 'Telegram Bot',
|
||||||
@@ -290,7 +288,8 @@ const en = {
|
|||||||
to: 'To',
|
to: 'To',
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
include: 'Include',
|
include: 'Include',
|
||||||
delAllEmailConfirm: 'Do you really want to delete it?'
|
delAllEmailConfirm: 'Do you really want to delete it?',
|
||||||
|
s3Configuration: 'S3 Configuration'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default en
|
export default en
|
||||||
|
|||||||
@@ -136,8 +136,6 @@ const zh = {
|
|||||||
loginDomain: '登录框域名',
|
loginDomain: '登录框域名',
|
||||||
multipleEmail: '多号模式',
|
multipleEmail: '多号模式',
|
||||||
multipleEmailDesc: '开启后账号栏出现一个用户可以添加多个邮箱',
|
multipleEmailDesc: '开启后账号栏出现一个用户可以添加多个邮箱',
|
||||||
physicallyWipeData: '物理清空数据',
|
|
||||||
physicallyWipeDataDesc: '该操作会物理清空所有已被删除的数据',
|
|
||||||
customization: '个性化设置',
|
customization: '个性化设置',
|
||||||
websiteTitle: '网站标题',
|
websiteTitle: '网站标题',
|
||||||
loginBoxOpacity: '登录透明',
|
loginBoxOpacity: '登录透明',
|
||||||
@@ -147,8 +145,8 @@ const zh = {
|
|||||||
autoRefresh: '自动刷新',
|
autoRefresh: '自动刷新',
|
||||||
autoRefreshDesc: '轮询请求服务器获取最新邮件',
|
autoRefreshDesc: '轮询请求服务器获取最新邮件',
|
||||||
sendEmail: '邮件发送',
|
sendEmail: '邮件发送',
|
||||||
resendToken: '添加 Resend Token',
|
resendToken: 'Resend Token',
|
||||||
R2OS: 'R2 对象存储',
|
oss: '对象存储',
|
||||||
osDomain: '访问域名',
|
osDomain: '访问域名',
|
||||||
emailPush: '邮件推送',
|
emailPush: '邮件推送',
|
||||||
tgBot: 'Telegram 机器人',
|
tgBot: 'Telegram 机器人',
|
||||||
@@ -211,7 +209,7 @@ const zh = {
|
|||||||
addRoleTitle: '添加身份',
|
addRoleTitle: '添加身份',
|
||||||
emptyUserNameMsg: '用户名不能为空',
|
emptyUserNameMsg: '用户名不能为空',
|
||||||
delAccountConfirm: '确认删除当前账号及所有数据吗?',
|
delAccountConfirm: '确认删除当前账号及所有数据吗?',
|
||||||
clearAllDelConfirm: '此操作不可逆转, 输入 <b style="font-weight: bold">确认删除</b> 继续操作',
|
clearAllDelConfirm: '此操作不可逆转, 输入 <b style="font-weight: bold;">确认删除</b> 继续操作',
|
||||||
warning: '警告',
|
warning: '警告',
|
||||||
delInputPattern: '确认删除',
|
delInputPattern: '确认删除',
|
||||||
inputErrorMessage: '请输入确认删除',
|
inputErrorMessage: '请输入确认删除',
|
||||||
@@ -290,7 +288,8 @@ const zh = {
|
|||||||
to: '至',
|
to: '至',
|
||||||
clear: '清除',
|
clear: '清除',
|
||||||
include: '包含',
|
include: '包含',
|
||||||
delAllEmailConfirm: '确定要删除吗?'
|
delAllEmailConfirm: '确定要删除吗?',
|
||||||
|
s3Configuration: 'S3 配置'
|
||||||
|
|
||||||
}
|
}
|
||||||
export default zh
|
export default zh
|
||||||
|
|||||||
@@ -61,20 +61,7 @@
|
|||||||
v-model="setting.manyEmail"/>
|
v-model="setting.manyEmail"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
|
||||||
<div>
|
|
||||||
<span>{{ $t('physicallyWipeData') }}</span>
|
|
||||||
<el-tooltip effect="dark" :content="$t('physicallyWipeDataDesc')">
|
|
||||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button class="opt-button" style="margin-top: 0" @click="physicsDeleteAllData" size="small"
|
|
||||||
type="primary">
|
|
||||||
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -196,9 +183,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- R2 Object Storage Card -->
|
<!-- Object Storage Card -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<div class="card-title">{{ $t('R2OS') }}</div>
|
<div class="card-title">{{ $t('oss') }}</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div><span>{{ $t('osDomain') }}</span></div>
|
<div><span>{{ $t('osDomain') }}</span></div>
|
||||||
@@ -210,10 +197,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div><span>{{ $t('S3配置') }}</span></div>
|
<div><span>{{ $t('s3Configuration') }}</span></div>
|
||||||
<div class="r2domain">
|
<div class="r2domain">
|
||||||
<el-button class="opt-button" size="small" type="primary" @click="ossConfigShow = true">
|
<el-button @mouseenter="s3IsDisabled = true"
|
||||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
@mouseleave="s3IsDisabled = false"
|
||||||
|
:disabled="setting.hasR2 && s3IsDisabled" class="opt-button" size="small" type="primary" @click="addS3Show = true">
|
||||||
|
<Icon icon="fluent:settings-48-regular" width="16" height="16"/>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -642,6 +631,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<el-dialog v-model="addS3Show" :title="t('s3Configuration')" width="340" @closed="resetAddS3Form">
|
||||||
|
<form>
|
||||||
|
<el-input class="dialog-input" type="text" placeholder="Bucket" v-model="s3.bucket"/>
|
||||||
|
<el-input class="dialog-input" type="text" placeholder="Endpoint" v-model="s3.endpoint"/>
|
||||||
|
<el-input class="dialog-input" type="text" placeholder="Region" v-model="s3.region"/>
|
||||||
|
<el-input class="dialog-input" type="text" :placeholder="setting.s3AccessKey || 'Access Key'"
|
||||||
|
v-model="s3.s3AccessKey"/>
|
||||||
|
<el-input type="text" :placeholder="setting.s3SecretKey || 'Secret Key'" v-model="s3.s3SecretKey"/>
|
||||||
|
<div class="s3-button">
|
||||||
|
<el-button :loading="clearS3Loading" @click="clearS3">{{ t('clear') }}</el-button>
|
||||||
|
<el-button type="primary" :loading="settingLoading && !clearS3Loading" @click="saveS3">{{ t('save') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</el-dialog>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -680,7 +683,6 @@ const userStore = useUserStore();
|
|||||||
const editTitleShow = ref(false)
|
const editTitleShow = ref(false)
|
||||||
const resendTokenFormShow = ref(false)
|
const resendTokenFormShow = ref(false)
|
||||||
const r2DomainShow = ref(false)
|
const r2DomainShow = ref(false)
|
||||||
const ossConfigShow = ref(false)
|
|
||||||
const turnstileShow = ref(false)
|
const turnstileShow = ref(false)
|
||||||
const tgSettingShow = ref(false)
|
const tgSettingShow = ref(false)
|
||||||
const noticePopupShow = ref(false)
|
const noticePopupShow = ref(false)
|
||||||
@@ -692,14 +694,17 @@ const uiStore = useUiStore();
|
|||||||
const {settings: setting} = storeToRefs(settingStore);
|
const {settings: setting} = storeToRefs(settingStore);
|
||||||
const editTitle = ref('')
|
const editTitle = ref('')
|
||||||
const settingLoading = ref(false)
|
const settingLoading = ref(false)
|
||||||
|
const clearS3Loading = ref(false)
|
||||||
const r2DomainInput = ref('')
|
const r2DomainInput = ref('')
|
||||||
const loginOpacity = ref(0)
|
const loginOpacity = ref(0)
|
||||||
const backgroundUrl = ref('')
|
const backgroundUrl = ref('')
|
||||||
let backgroundFile = {}
|
let backgroundFile = {}
|
||||||
|
const s3IsDisabled = ref(false)
|
||||||
const showSetBackground = ref(false)
|
const showSetBackground = ref(false)
|
||||||
let regVerifyCount = ref(1)
|
let regVerifyCount = ref(1)
|
||||||
let addVerifyCount = ref(1)
|
let addVerifyCount = ref(1)
|
||||||
let backup = '{}'
|
let backup = '{}'
|
||||||
|
const addS3Show = ref(false)
|
||||||
const addVerifyCountShow = ref(false)
|
const addVerifyCountShow = ref(false)
|
||||||
const regVerifyCountShow = ref(false)
|
const regVerifyCountShow = ref(false)
|
||||||
const resendTokenForm = reactive({
|
const resendTokenForm = reactive({
|
||||||
@@ -711,6 +716,14 @@ const turnstileForm = reactive({
|
|||||||
secretKey: ''
|
secretKey: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const s3 = reactive({
|
||||||
|
bucket: '',
|
||||||
|
endpoint: '',
|
||||||
|
region: '',
|
||||||
|
s3AccessKey: '',
|
||||||
|
s3SecretKey: ''
|
||||||
|
})
|
||||||
|
|
||||||
const noticeForm = reactive({
|
const noticeForm = reactive({
|
||||||
noticeTitle: '',
|
noticeTitle: '',
|
||||||
noticeContent: '',
|
noticeContent: '',
|
||||||
@@ -763,14 +776,8 @@ function getSettings() {
|
|||||||
r2DomainInput.value = setting.value.r2Domain
|
r2DomainInput.value = setting.value.r2Domain
|
||||||
addVerifyCount.value = setting.value.addVerifyCount
|
addVerifyCount.value = setting.value.addVerifyCount
|
||||||
regVerifyCount.value = setting.value.regVerifyCount
|
regVerifyCount.value = setting.value.regVerifyCount
|
||||||
noticeForm.notice = setting.value.notice
|
resetNoticeForm()
|
||||||
noticeForm.noticeContent = setting.value.noticeContent
|
resetAddS3Form()
|
||||||
noticeForm.noticeDuration = setting.value.noticeDuration
|
|
||||||
noticeForm.noticeTitle = setting.value.noticeTitle
|
|
||||||
noticeForm.noticePosition = setting.value.noticePosition
|
|
||||||
noticeForm.noticeType = setting.value.noticeType
|
|
||||||
noticeForm.noticeOffset = setting.value.noticeOffset
|
|
||||||
noticeForm.noticeWidth = setting.value.noticeWidth
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,6 +796,14 @@ function openRegVerifyCount() {
|
|||||||
regVerifyCountShow.value = true
|
regVerifyCountShow.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetAddS3Form() {
|
||||||
|
s3.bucket = setting.value.bucket
|
||||||
|
s3.endpoint = setting.value.endpoint
|
||||||
|
s3.region = setting.value.region
|
||||||
|
s3.s3AccessKey = ''
|
||||||
|
s3.s3SecretKey = ''
|
||||||
|
}
|
||||||
|
|
||||||
const resendList = computed(() => {
|
const resendList = computed(() => {
|
||||||
|
|
||||||
let list = Object.keys(setting.value.resendTokens).map(key => {
|
let list = Object.keys(setting.value.resendTokens).map(key => {
|
||||||
@@ -958,6 +973,33 @@ function addChatTag(val) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearS3() {
|
||||||
|
|
||||||
|
const form = {
|
||||||
|
bucket: '',
|
||||||
|
endpoint: '',
|
||||||
|
region: '',
|
||||||
|
s3AccessKey: '',
|
||||||
|
s3SecretKey: ''
|
||||||
|
}
|
||||||
|
clearS3Loading.value = true
|
||||||
|
editSetting(form)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveS3() {
|
||||||
|
|
||||||
|
const form = {
|
||||||
|
bucket: s3.bucket,
|
||||||
|
endpoint: s3.endpoint,
|
||||||
|
region: s3.region
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s3.s3AccessKey) form.s3AccessKey = s3.s3AccessKey
|
||||||
|
if (s3.s3SecretKey) form.s3SecretKey = s3.s3SecretKey
|
||||||
|
|
||||||
|
editSetting(form)
|
||||||
|
}
|
||||||
|
|
||||||
function tgBotSave() {
|
function tgBotSave() {
|
||||||
const form = {
|
const form = {
|
||||||
tgBotToken: tgBotToken.value,
|
tgBotToken: tgBotToken.value,
|
||||||
@@ -1125,6 +1167,8 @@ function change(e) {
|
|||||||
const settingForm = {...setting.value}
|
const settingForm = {...setting.value}
|
||||||
delete settingForm.siteKey
|
delete settingForm.siteKey
|
||||||
delete settingForm.secretKey
|
delete settingForm.secretKey
|
||||||
|
delete settingForm.s3AccessKey
|
||||||
|
delete settingForm.s3SecretKey
|
||||||
delete settingForm.resendTokens
|
delete settingForm.resendTokens
|
||||||
editSetting(settingForm, false)
|
editSetting(settingForm, false)
|
||||||
}
|
}
|
||||||
@@ -1167,11 +1211,13 @@ function editSetting(settingForm, refreshStatus = true) {
|
|||||||
addVerifyCountShow.value = false
|
addVerifyCountShow.value = false
|
||||||
regVerifyCountShow.value = false
|
regVerifyCountShow.value = false
|
||||||
noticePopupShow.value = false
|
noticePopupShow.value = false
|
||||||
|
addS3Show.value = false
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
loginOpacity.value = setting.value.loginOpacity
|
loginOpacity.value = setting.value.loginOpacity
|
||||||
setting.value = {...setting.value, ...JSON.parse(backup)}
|
setting.value = {...setting.value, ...JSON.parse(backup)}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
settingLoading.value = false
|
settingLoading.value = false
|
||||||
|
clearS3Loading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1492,6 +1538,16 @@ function editSetting(settingForm, refreshStatus = true) {
|
|||||||
width: fit-content !important;
|
width: fit-content !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.s3-button {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.r2domain {
|
.r2domain {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
@@ -1523,6 +1579,10 @@ function editSetting(settingForm, refreshStatus = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-input {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.concerning-item {
|
.concerning-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ export default defineConfig(({mode}) => {
|
|||||||
base: env.VITE_STATIC_URL || '/',
|
base: env.VITE_STATIC_URL || '/',
|
||||||
plugins: [vue(),
|
plugins: [vue(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'autoUpdate', // 配置 service worker 的注册方式
|
registerType: 'autoUpdate',
|
||||||
includeAssets: ['favicon.svg', 'robots.txt'], // 指定需要包含的静态资源
|
|
||||||
|
includeAssets: ['favicon.svg', 'robots.txt'],
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Cloud Mail',
|
name: 'Cloud Mail',
|
||||||
short_name: 'Cloud Mail',
|
short_name: 'Cloud Mail',
|
||||||
@@ -26,14 +27,26 @@ export default defineConfig(({mode}) => {
|
|||||||
theme_color: '#FFFFFF',
|
theme_color: '#FFFFFF',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: 'mail-192.png',//像素尺寸一定要对应
|
src: 'mail-192.png',
|
||||||
sizes: '192x192',
|
sizes: '192x192',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
src: 'mail-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
src: 'mail-512.png',
|
src: 'mail-512.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
|
purpose: 'any'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'mail-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable'
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
1772
mail-worker/package-lock.json
generated
1772
mail-worker/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wrangler dev --config wrangler-dev.toml",
|
"dev": "wrangler dev --config wrangler-dev.toml",
|
||||||
"test": "wrangler deploy --config wrangler-test.toml",
|
"test": "wrangler deploy --config wrangler-test.toml",
|
||||||
"deploy": "wrangler deploy",
|
"deploy": "npm --prefix ../mail-vue run build && wrangler deploy",
|
||||||
"start": "wrangler dev"
|
"start": "wrangler dev",
|
||||||
|
"build": "npm --prefix ../mail-vue run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vitest-pool-workers": "^0.7.5",
|
"@cloudflare/vitest-pool-workers": "^0.7.5",
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
"wrangler": "^4.7.0"
|
"wrangler": "^4.7.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.879.0",
|
||||||
"@cloudflare/vite-plugin": "1.6.0",
|
"@cloudflare/vite-plugin": "1.6.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"drizzle-orm": "^0.42.0",
|
"drizzle-orm": "^0.42.0",
|
||||||
|
|||||||
@@ -21,8 +21,3 @@ app.put('/setting/setBackground', async (c) => {
|
|||||||
const key = await settingService.setBackground(c, await c.req.json());
|
const key = await settingService.setBackground(c, await c.req.json());
|
||||||
return c.json(result.ok(key));
|
return c.json(result.ok(key));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/setting/physicsDeleteAll', async (c) => {
|
|
||||||
await settingService.physicsDeleteAll(c);
|
|
||||||
return c.json(result.ok());
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
import app from '../hono/hono';
|
|
||||||
import result from '../model/result';
|
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export const setting = sqliteTable('setting', {
|
|||||||
noticeWidth: integer('notice_width').default(400).notNull(),
|
noticeWidth: integer('notice_width').default(400).notNull(),
|
||||||
notice: integer('notice').default(0).notNull(),
|
notice: integer('notice').default(0).notNull(),
|
||||||
noRecipient: integer('no_recipient').default(1).notNull(),
|
noRecipient: integer('no_recipient').default(1).notNull(),
|
||||||
loginDomain: integer('login_domain').default(0).notNull()
|
loginDomain: integer('login_domain').default(0).notNull(),
|
||||||
|
bucket: text('bucket').default('').notNull(),
|
||||||
|
region: text('region').default('').notNull(),
|
||||||
|
endpoint: text('endpoint').default('').notNull(),
|
||||||
|
s3AccessKey: text('s3_access_key').default('').notNull(),
|
||||||
|
s3SecretKey: text('s3_secret_key').default('').notNull()
|
||||||
});
|
});
|
||||||
export default setting
|
export default setting
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ const en = {
|
|||||||
delMyAccount: 'Cannot delete your own account',
|
delMyAccount: 'Cannot delete your own account',
|
||||||
noUserAccount: 'This email does not belong to the current user',
|
noUserAccount: 'This email does not belong to the current user',
|
||||||
usernameLengthLimit: 'Username length exceeds the limit',
|
usernameLengthLimit: 'Username length exceeds the limit',
|
||||||
noOsDomainSendPic: 'Cannot send body images: R2 domain not configured',
|
noOsDomainSendPic: 'Cannot send body images: object storage domain not configured',
|
||||||
noOsSendPic: 'Cannot send body images: R2 object storage not configured',
|
noOsSendPic: 'Cannot send body images: object storage not configured',
|
||||||
noOsDomainSendAtt: 'Cannot send attachments: R2 domain not configured',
|
noOsDomainSendAtt: 'Cannot send attachments: object storage domain not configured',
|
||||||
noOsSendAtt: 'Cannot send attachments: R2 object storage not configured',
|
noOsSendAtt: 'Cannot send attachments: object storage not configured',
|
||||||
disabledSend: 'Email sending feature is disabled',
|
disabledSend: 'Email sending feature is disabled',
|
||||||
noSeparateSend: 'Attachments are not supported in separate sending',
|
noSeparateSend: 'Attachments are not supported in separate sending',
|
||||||
daySendLimit: 'Daily send limit reached',
|
daySendLimit: 'Daily send limit reached',
|
||||||
@@ -46,8 +46,8 @@ const en = {
|
|||||||
delDefRole: 'Default role cannot be deleted',
|
delDefRole: 'Default role cannot be deleted',
|
||||||
notJsonDomain: 'Environment variable "domain" must be in JSON format',
|
notJsonDomain: 'Environment variable "domain" must be in JSON format',
|
||||||
noDomainVariable: 'Environment variable domain cannot be empty',
|
noDomainVariable: 'Environment variable domain cannot be empty',
|
||||||
noOsUpBack: 'Cannot upload background: R2 object storage not configured',
|
noOsUpBack: 'Cannot upload background: object storage not configured',
|
||||||
noOsDomainUpBack: 'Cannot upload background: R2 domain not configured',
|
noOsDomainUpBack: 'Cannot upload background: object storage domain not configured',
|
||||||
starNotExistEmail: 'Starred email does not exist',
|
starNotExistEmail: 'Starred email does not exist',
|
||||||
emptyBotToken: 'Please verify that you are human',
|
emptyBotToken: 'Please verify that you are human',
|
||||||
botVerifyFail: 'Bot verification failed, please try again',
|
botVerifyFail: 'Bot verification failed, please try again',
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ const zh = {
|
|||||||
delMyAccount: '不可以删除自己的邮箱',
|
delMyAccount: '不可以删除自己的邮箱',
|
||||||
noUserAccount: '该邮箱不属于当前用户',
|
noUserAccount: '该邮箱不属于当前用户',
|
||||||
usernameLengthLimit: '用户名长度超出限制',
|
usernameLengthLimit: '用户名长度超出限制',
|
||||||
noOsDomainSendPic: 'r2域名未配置不能发送正文图片',
|
noOsDomainSendPic: '对象存储域名未配置不能发送正文图片',
|
||||||
noOsSendPic: 'r2对象存储未配置不能发送正文图片',
|
noOsSendPic: '对象存储未配置不能发送正文图片',
|
||||||
noOsDomainSendAtt: 'r2域名未配置不能发送附件',
|
noOsDomainSendAtt: '域名未配置不能发送附件',
|
||||||
noOsSendAtt: 'r2对象存储未配置不能发送附件',
|
noOsSendAtt: '对象存储未配置不能发送附件',
|
||||||
disabledSend: '邮件发送功能已停用',
|
disabledSend: '邮件发送功能已停用',
|
||||||
noSeparateSend: '分别发送暂时不支持附件',
|
noSeparateSend: '分别发送暂时不支持附件',
|
||||||
daySendLimit: '发送次数已到达每日限制',
|
daySendLimit: '发送次数已到达每日限制',
|
||||||
@@ -46,8 +46,8 @@ const zh = {
|
|||||||
delDefRole: '默认身份不能删除',
|
delDefRole: '默认身份不能删除',
|
||||||
notJsonDomain: '环境变量domain必须是JSON类型',
|
notJsonDomain: '环境变量domain必须是JSON类型',
|
||||||
noDomainVariable: '环境变量domain不能为空',
|
noDomainVariable: '环境变量domain不能为空',
|
||||||
noOsUpBack: 'r2对象存储未配置不能上传背景',
|
noOsUpBack: '对象存储未配置不能上传背景',
|
||||||
noOsDomainUpBack: 'r2域名未配置不能上传背景',
|
noOsDomainUpBack: '对象存储域名未配置不能上传背景',
|
||||||
starNotExistEmail: '星标的邮件不存在',
|
starNotExistEmail: '星标的邮件不存在',
|
||||||
emptyBotToken: '需要进行人机验证',
|
emptyBotToken: '需要进行人机验证',
|
||||||
botVerifyFail: '人机验证失败,请重试',
|
botVerifyFail: '人机验证失败,请重试',
|
||||||
|
|||||||
@@ -21,10 +21,26 @@ const init = {
|
|||||||
await this.v1_5DB(c);
|
await this.v1_5DB(c);
|
||||||
await this.v1_6DB(c);
|
await this.v1_6DB(c);
|
||||||
await this.v1_7DB(c);
|
await this.v1_7DB(c);
|
||||||
|
await this.v1_7DB(c);
|
||||||
|
await this.v2DB(c);
|
||||||
await settingService.refresh(c);
|
await settingService.refresh(c);
|
||||||
return c.text(t('initSuccess'));
|
return c.text(t('initSuccess'));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async v2DB(c) {
|
||||||
|
try {
|
||||||
|
await c.env.db.batch([
|
||||||
|
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN bucket TEXT NOT NULL DEFAULT '';`),
|
||||||
|
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN region TEXT NOT NULL DEFAULT '';`),
|
||||||
|
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN endpoint TEXT NOT NULL DEFAULT '';`),
|
||||||
|
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN s3_access_key TEXT NOT NULL DEFAULT '';`),
|
||||||
|
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN s3_secret_key TEXT NOT NULL DEFAULT '';`)
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async v1_7DB(c) {
|
async v1_7DB(c) {
|
||||||
try {
|
try {
|
||||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN login_domain INTEGER NOT NULL DEFAULT 0;`).run();
|
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN login_domain INTEGER NOT NULL DEFAULT 0;`).run();
|
||||||
@@ -308,7 +324,6 @@ const init = {
|
|||||||
(17, '系统设置', '', 0, 1, 6),
|
(17, '系统设置', '', 0, 1, 6),
|
||||||
(18, '设置查看', 'setting:query', 17, 2, 0),
|
(18, '设置查看', 'setting:query', 17, 2, 0),
|
||||||
(19, '设置修改', 'setting:set', 17, 2, 1),
|
(19, '设置修改', 'setting:set', 17, 2, 1),
|
||||||
(20, '物理清空', 'setting:clean', 17, 2, 2),
|
|
||||||
(21, '邮箱侧栏', '', 0, 0, 1),
|
(21, '邮箱侧栏', '', 0, 0, 1),
|
||||||
(22, '邮箱查看', 'account:query', 21, 2, 0),
|
(22, '邮箱查看', 'account:query', 21, 2, 0),
|
||||||
(23, '邮箱添加', 'account:add', 21, 2, 1),
|
(23, '邮箱添加', 'account:add', 21, 2, 1),
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const requirePerms = [
|
|||||||
'/role/setDefault',
|
'/role/setDefault',
|
||||||
'/allEmail/list',
|
'/allEmail/list',
|
||||||
'/allEmail/delete',
|
'/allEmail/delete',
|
||||||
'/setting/physicsDeleteAll',
|
|
||||||
'/setting/setBackground',
|
'/setting/setBackground',
|
||||||
'/setting/set',
|
'/setting/set',
|
||||||
'/setting/query',
|
'/setting/query',
|
||||||
@@ -73,7 +72,6 @@ const premKey = {
|
|||||||
'all-email:delete': ['/allEmail/delete','/allEmail/batchDelete'],
|
'all-email:delete': ['/allEmail/delete','/allEmail/batchDelete'],
|
||||||
'setting:query': ['/setting/query'],
|
'setting:query': ['/setting/query'],
|
||||||
'setting:set': ['/setting/set', '/setting/setBackground'],
|
'setting:set': ['/setting/set', '/setting/setBackground'],
|
||||||
'setting:clean': ['/setting/physicsDeleteAll'],
|
|
||||||
'analysis:query': ['/analysis/echarts'],
|
'analysis:query': ['/analysis/echarts'],
|
||||||
'reg-key:add': ['/regKey/add'],
|
'reg-key:add': ['/regKey/add'],
|
||||||
'reg-key:query': ['/regKey/list','/regKey/history'],
|
'reg-key:query': ['/regKey/list','/regKey/history'],
|
||||||
|
|||||||
@@ -156,19 +156,6 @@ const accountService = {
|
|||||||
await orm(c).insert(account).values(list).run();
|
await orm(c).insert(account).values(list).run();
|
||||||
},
|
},
|
||||||
|
|
||||||
async physicsDeleteAll(c) {
|
|
||||||
const accountIdsRow = await orm(c).select({accountId: account.accountId}).from(account).where(eq(account.isDel,isDel.DELETE)).limit(99);
|
|
||||||
if (accountIdsRow.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const accountIds = accountIdsRow.map(item => item.accountId)
|
|
||||||
await emailService.physicsDeleteAccountIds(c, accountIds);
|
|
||||||
await orm(c).delete(account).where(inArray(account.accountId,accountIds)).run();
|
|
||||||
if (accountIdsRow.length === 99) {
|
|
||||||
await this.physicsDeleteAll(c)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async physicsDeleteByUserIds(c, userIds) {
|
async physicsDeleteByUserIds(c, userIds) {
|
||||||
await emailService.physicsDeleteUserIds(c, userIds);
|
await emailService.physicsDeleteUserIds(c, userIds);
|
||||||
await orm(c).delete(account).where(inArray(account.userId,userIds)).run();
|
await orm(c).delete(account).where(inArray(account.userId,userIds)).run();
|
||||||
|
|||||||
@@ -117,63 +117,45 @@ const attService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async removeByUserIds(c, userIds) {
|
async removeByUserIds(c, userIds) {
|
||||||
await this.removeAttByField(c, att.userId, userIds);
|
await this.removeAttByField(c, 'user_id', userIds);
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeByEmailIds(c, emailIds) {
|
async removeByEmailIds(c, emailIds) {
|
||||||
await this.removeAttByField(c, att.emailId, emailIds);
|
await this.removeAttByField(c, 'email_id', emailIds);
|
||||||
},
|
|
||||||
|
|
||||||
async removeByAccountIds(c, accountIds) {
|
|
||||||
await this.removeAttByField(c, att.accountId, accountIds);
|
|
||||||
},
|
|
||||||
|
|
||||||
async removeAttByField(c, fieldName, fieldValues) {
|
|
||||||
|
|
||||||
const condition = inArray(fieldName, fieldValues);
|
|
||||||
const attList = await orm(c).select().from(att).where(condition).limit(99);
|
|
||||||
|
|
||||||
if (attList.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attIds = attList.map(attRow => attRow.attId);
|
|
||||||
const keys = attList.map(attRow => attRow.key);
|
|
||||||
await orm(c).delete(att).where(inArray(att.attId, attIds)).run();
|
|
||||||
|
|
||||||
const existAttRows = await orm(c).select().from(att).where(inArray(att.key, keys)).all();
|
|
||||||
const existKeys = existAttRows.map(attRow => attRow.key);
|
|
||||||
const delKeyList = keys.filter(key => !existKeys.includes(key));
|
|
||||||
if (delKeyList.length > 0) {
|
|
||||||
await c.env.r2.delete(delKeyList);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attList.length >= 99) {
|
|
||||||
await this.removeAttByField(c, fieldName, fieldValues);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
selectByEmailIds(c, emailIds) {
|
selectByEmailIds(c, emailIds) {
|
||||||
return orm(c).select().from(att).where(
|
return orm(c).select().from(att).where(
|
||||||
and(
|
and(
|
||||||
inArray(att.emailId,emailIds),
|
inArray(att.emailId, emailIds),
|
||||||
eq(att.type, attConst.type.ATT)
|
eq(att.type, attConst.type.ATT)
|
||||||
))
|
))
|
||||||
.all();
|
.all();
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteByEmailIds(c, emailIds) {
|
async removeAttByField(c, fieldName, fieldValues) {
|
||||||
|
|
||||||
|
const queryAttSql = fieldValues.map(value =>
|
||||||
|
c.env.db.prepare(`SELECT a.key, a.att_id
|
||||||
|
FROM attachments a
|
||||||
|
JOIN (SELECT key
|
||||||
|
FROM attachments
|
||||||
|
GROUP BY key
|
||||||
|
HAVING COUNT (*) = 1) t
|
||||||
|
ON a.key = t.key
|
||||||
|
WHERE a.${fieldName} = ?;`).bind(value));
|
||||||
|
|
||||||
const queryAttSql = emailIds.map(emailId => c.env.db.prepare(`SELECT key,att_id FROM attachments WHERE email_id = ${emailId} GROUP BY key HAVING COUNT(*) = 1;`))
|
|
||||||
const attListResult = await c.env.db.batch(queryAttSql);
|
const attListResult = await c.env.db.batch(queryAttSql);
|
||||||
|
|
||||||
const delKeyList = attListResult.flatMap(r => r.results.map(row => row.key));
|
const delKeyList = attListResult.flatMap(r => r.results.map(row => row.key));
|
||||||
|
|
||||||
if (delKeyList.length > 0) {
|
if (delKeyList.length > 0) {
|
||||||
await this.batchDelete(c, delKeyList)
|
await this.batchDelete(c, delKeyList);
|
||||||
}
|
}
|
||||||
|
|
||||||
const delAttSql = emailIds.map(emailId => c.env.db.prepare(`DELETE FROM attachments WHERE email_id = ${emailId}`))
|
const delAttSql = fieldValues.map(value => c.env.db.prepare(`DELETE
|
||||||
|
FROM attachments
|
||||||
|
WHERE ${fieldName} = ?`).bind(value));
|
||||||
await c.env.db.batch(delAttSql);
|
await c.env.db.batch(delAttSql);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import starService from './star-service';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import kvConst from '../const/kv-const';
|
import kvConst from '../const/kv-const';
|
||||||
import { t } from '../i18n/i18n'
|
import { t } from '../i18n/i18n'
|
||||||
|
import r2Service from './r2-service';
|
||||||
|
|
||||||
const emailService = {
|
const emailService = {
|
||||||
|
|
||||||
@@ -128,7 +129,18 @@ const emailService = {
|
|||||||
|
|
||||||
async send(c, params, userId) {
|
async send(c, params, userId) {
|
||||||
|
|
||||||
let { accountId, name, sendType, emailId, receiveEmail, manyType, text, content, subject, attachments } = params;
|
let {
|
||||||
|
accountId,
|
||||||
|
name,
|
||||||
|
sendType,
|
||||||
|
emailId,
|
||||||
|
receiveEmail,
|
||||||
|
manyType,
|
||||||
|
text,
|
||||||
|
content,
|
||||||
|
subject,
|
||||||
|
attachments
|
||||||
|
} = params;
|
||||||
|
|
||||||
const { resendTokens, r2Domain, send } = await settingService.query(c);
|
const { resendTokens, r2Domain, send } = await settingService.query(c);
|
||||||
|
|
||||||
@@ -164,7 +176,7 @@ const emailService = {
|
|||||||
throw new BizError(t('noOsDomainSendPic'));
|
throw new BizError(t('noOsDomainSendPic'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attDataList.length > 0 && !c.env.r2) {
|
if (attDataList.length > 0 && !await r2Service.hasOSS(c)) {
|
||||||
throw new BizError(t('noOsSendPic'));
|
throw new BizError(t('noOsSendPic'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +184,7 @@ const emailService = {
|
|||||||
throw new BizError(t('noOsDomainSendAtt'));
|
throw new BizError(t('noOsDomainSendAtt'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachments.length > 0 && !c.env.r2) {
|
if (attachments.length > 0 && !await r2Service.hasOSS(c)) {
|
||||||
throw new BizError(t('noOsSendAtt'));
|
throw new BizError(t('noOsSendAtt'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +354,7 @@ const emailService = {
|
|||||||
await attService.saveArticleAtt(c, attDataList, userId, accountId, emailRow.emailId);
|
await attService.saveArticleAtt(c, attDataList, userId, accountId, emailRow.emailId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachments?.length > 0 && c.env.r2) {
|
if (attachments?.length > 0 && await r2Service.hasOSS(c)) {
|
||||||
await attService.saveSendAtt(c, attachments, userId, accountId, emailRow.emailId);
|
await attService.saveSendAtt(c, attachments, userId, accountId, emailRow.emailId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,21 +455,6 @@ const emailService = {
|
|||||||
return list;
|
return list;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
async physicsDeleteAll(c) {
|
|
||||||
const emailIdsRow = await orm(c).select({ emailId: email.emailId }).from(email).where(eq(email.isDel, isDel.DELETE)).limit(99);
|
|
||||||
if (emailIdsRow.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const emailIds = emailIdsRow.map(row => row.emailId);
|
|
||||||
await attService.removeByEmailIds(c, emailIds);
|
|
||||||
await starService.removeByEmailIds(c, emailIds);
|
|
||||||
await orm(c).delete(email).where(inArray(email.emailId, emailIds)).run();
|
|
||||||
if (emailIdsRow.length === 99) {
|
|
||||||
await this.physicsDeleteAll(c);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async physicsDelete(c, params) {
|
async physicsDelete(c, params) {
|
||||||
let { emailIds } = params;
|
let { emailIds } = params;
|
||||||
emailIds = emailIds.split(',').map(Number);
|
emailIds = emailIds.split(',').map(Number);
|
||||||
@@ -466,11 +463,6 @@ const emailService = {
|
|||||||
await orm(c).delete(email).where(inArray(email.emailId, emailIds)).run();
|
await orm(c).delete(email).where(inArray(email.emailId, emailIds)).run();
|
||||||
},
|
},
|
||||||
|
|
||||||
async physicsDeleteAccountIds(c, accountIds) {
|
|
||||||
await attService.removeByAccountIds(c, accountIds);
|
|
||||||
await orm(c).delete(email).where(inArray(email.accountId, accountIds)).run();
|
|
||||||
},
|
|
||||||
|
|
||||||
async physicsDeleteUserIds(c, userIds) {
|
async physicsDeleteUserIds(c, userIds) {
|
||||||
await attService.removeByUserIds(c, userIds);
|
await attService.removeByUserIds(c, userIds);
|
||||||
await orm(c).delete(email).where(inArray(email.userId, userIds)).run();
|
await orm(c).delete(email).where(inArray(email.userId, userIds)).run();
|
||||||
@@ -657,7 +649,7 @@ const emailService = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await attService.deleteByEmailIds(c, emailIds);
|
await attService.removeByEmailIds(c, emailIds);
|
||||||
|
|
||||||
await orm(c).delete(email).where(conditions.length > 1 ? and(...conditions) : conditions[0]).run();
|
await orm(c).delete(email).where(conditions.length > 1 ? and(...conditions) : conditions[0]).run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,34 @@
|
|||||||
|
import s3Service from './s3-service';
|
||||||
|
import settingService from './setting-service';
|
||||||
|
|
||||||
const r2Service = {
|
const r2Service = {
|
||||||
|
|
||||||
|
async hasOSS(c) {
|
||||||
|
|
||||||
|
if (c.env.r2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setting = await settingService.query(c);
|
||||||
|
const { bucket, region, endpoint, s3AccessKey, s3SecretKey } = setting;
|
||||||
|
|
||||||
|
return !!(bucket && region && endpoint && s3AccessKey && s3SecretKey);
|
||||||
|
},
|
||||||
|
|
||||||
async putObj(c, key, content, metadata) {
|
async putObj(c, key, content, metadata) {
|
||||||
await c.env.r2.put(key, content, {
|
|
||||||
httpMetadata: {...metadata}
|
if (c.env.r2) {
|
||||||
});
|
|
||||||
|
await c.env.r2.put(key, content, {
|
||||||
|
httpMetadata: { ...metadata }
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
await s3Service.putObj(c, key, content, metadata);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getObj(c, key) {
|
async getObj(c, key) {
|
||||||
@@ -10,7 +36,17 @@ const r2Service = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async delete(c, key) {
|
async delete(c, key) {
|
||||||
await c.env.r2.delete(key);
|
|
||||||
|
if (c.env.r2) {
|
||||||
|
|
||||||
|
await c.env.r2.delete(key);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
await s3Service.deleteObj(c, key);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
54
mail-worker/src/service/s3-service.js
Normal file
54
mail-worker/src/service/s3-service.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { S3Client, PutObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
||||||
|
import settingService from './setting-service';
|
||||||
|
import domainUtils from '../utils/domain-uitls';
|
||||||
|
const s3Service = {
|
||||||
|
|
||||||
|
async putObj(c, key, content, metadata) {
|
||||||
|
|
||||||
|
const client = await this.client(c);
|
||||||
|
|
||||||
|
const { bucket } = await settingService.query(c);
|
||||||
|
|
||||||
|
await client.send(
|
||||||
|
new PutObjectCommand({ Bucket: bucket, Key: key, Body: content, ...metadata })
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteObj(c,keys) {
|
||||||
|
|
||||||
|
if (typeof keys === 'string') {
|
||||||
|
keys = [keys]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await this.client(c)
|
||||||
|
|
||||||
|
const { bucket } = await settingService.query(c);
|
||||||
|
|
||||||
|
await client.send(
|
||||||
|
new DeleteObjectsCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Delete: {
|
||||||
|
Objects: keys.map(key => ({Key: key}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
async client(c) {
|
||||||
|
const { region, endpoint, s3AccessKey, s3SecretKey } = await settingService.query(c);
|
||||||
|
return new S3Client({
|
||||||
|
region: region,
|
||||||
|
endpoint: domainUtils.toOssDomain(endpoint),
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: s3AccessKey,
|
||||||
|
secretAccessKey: s3SecretKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default s3Service
|
||||||
@@ -17,12 +17,18 @@ const settingService = {
|
|||||||
async refresh(c) {
|
async refresh(c) {
|
||||||
const settingRow = await orm(c).select().from(setting).get();
|
const settingRow = await orm(c).select().from(setting).get();
|
||||||
settingRow.resendTokens = JSON.parse(settingRow.resendTokens);
|
settingRow.resendTokens = JSON.parse(settingRow.resendTokens);
|
||||||
|
c.set('setting', settingRow);
|
||||||
await c.env.kv.put(KvConst.SETTING, JSON.stringify(settingRow));
|
await c.env.kv.put(KvConst.SETTING, JSON.stringify(settingRow));
|
||||||
},
|
},
|
||||||
|
|
||||||
async query(c) {
|
async query(c) {
|
||||||
|
|
||||||
|
if (c.get('setting')) {
|
||||||
|
return c.get('setting')
|
||||||
|
}
|
||||||
|
|
||||||
const setting = await c.env.kv.get(KvConst.SETTING, { type: 'json' });
|
const setting = await c.env.kv.get(KvConst.SETTING, { type: 'json' });
|
||||||
|
|
||||||
let domainList = c.env.domain;
|
let domainList = c.env.domain;
|
||||||
|
|
||||||
if (typeof domainList === 'string') {
|
if (typeof domainList === 'string') {
|
||||||
@@ -39,6 +45,7 @@ const settingService = {
|
|||||||
|
|
||||||
domainList = domainList.map(item => '@' + item);
|
domainList = domainList.map(item => '@' + item);
|
||||||
setting.domainList = domainList;
|
setting.domainList = domainList;
|
||||||
|
c.set('setting', setting);
|
||||||
return setting;
|
return setting;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -49,11 +56,17 @@ const settingService = {
|
|||||||
verifyRecordService.selectListByIP(c)
|
verifyRecordService.selectListByIP(c)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
settingRow.siteKey = settingRow.siteKey ? `${settingRow.siteKey.slice(0, 12)}******` : null;
|
||||||
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******` : null;
|
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******` : null;
|
||||||
|
|
||||||
Object.keys(settingRow.resendTokens).forEach(key => {
|
Object.keys(settingRow.resendTokens).forEach(key => {
|
||||||
settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`;
|
settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
settingRow.s3AccessKey = settingRow.s3AccessKey ? `${settingRow.s3AccessKey.slice(0, 12)}******` : null;
|
||||||
|
settingRow.s3SecretKey = settingRow.s3SecretKey ? `${settingRow.s3SecretKey.slice(0, 12)}******` : null;
|
||||||
|
settingRow.hasR2 = !!c.env.r2
|
||||||
|
|
||||||
let regVerifyOpen = false
|
let regVerifyOpen = false
|
||||||
let addVerifyOpen = false
|
let addVerifyOpen = false
|
||||||
|
|
||||||
@@ -91,7 +104,7 @@ const settingService = {
|
|||||||
|
|
||||||
if (background && !background.startsWith('http')) {
|
if (background && !background.startsWith('http')) {
|
||||||
|
|
||||||
if (!c.env.r2) {
|
if (!await r2Service.hasOSS(c)) {
|
||||||
throw new BizError(t('noOsUpBack'));
|
throw new BizError(t('noOsUpBack'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,12 +137,6 @@ const settingService = {
|
|||||||
return background;
|
return background;
|
||||||
},
|
},
|
||||||
|
|
||||||
async physicsDeleteAll(c) {
|
|
||||||
await emailService.physicsDeleteAll(c);
|
|
||||||
await accountService.physicsDeleteAll(c);
|
|
||||||
await userService.physicsDeleteAll(c);
|
|
||||||
},
|
|
||||||
|
|
||||||
async websiteConfig(c) {
|
async websiteConfig(c) {
|
||||||
|
|
||||||
const settingRow = await this.get(c)
|
const settingRow = await this.get(c)
|
||||||
|
|||||||
@@ -87,20 +87,6 @@ const userService = {
|
|||||||
await c.env.kv.delete(kvConst.AUTH_INFO + userId)
|
await c.env.kv.delete(kvConst.AUTH_INFO + userId)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
async physicsDeleteAll(c) {
|
|
||||||
const userIdsRow = await orm(c).select().from(user).where(eq(user.isDel, isDel.DELETE)).limit(99);
|
|
||||||
if (userIdsRow.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const userIds = userIdsRow.map(item => item.userId);
|
|
||||||
await accountService.physicsDeleteByUserIds(c, userIds);
|
|
||||||
await orm(c).delete(user).where(inArray(user.userId, userIds)).run();
|
|
||||||
if (userIdsRow.length === 99) {
|
|
||||||
await this.physicsDeleteAll(c);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async physicsDelete(c, params) {
|
async physicsDelete(c, params) {
|
||||||
const { userId } = params
|
const { userId } = params
|
||||||
await accountService.physicsDeleteByUserIds(c, [userId])
|
await accountService.physicsDeleteByUserIds(c, [userId])
|
||||||
|
|||||||
20
mail-worker/src/utils/domain-uitls.js
Normal file
20
mail-worker/src/utils/domain-uitls.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const domainUtils = {
|
||||||
|
toOssDomain(domain) {
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domain.startsWith('http')) {
|
||||||
|
return 'https://' + domain
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.endsWith("/")) {
|
||||||
|
domain = domain.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default domainUtils
|
||||||
Reference in New Issue
Block a user