Compare commits

...

4 Commits

Author SHA1 Message Date
Dream Hunter
c0e870ce54 feat: all mail use MailBox Component (#173) 2024-04-28 15:58:26 +08:00
Dream Hunter
90e80fee53 feat: admin page add account mail count && sendbox default all && send access suppory filter (#172) 2024-04-28 14:02:33 +08:00
Dream Hunter
4fd7f776f6 feat: remove PREFIX logic in db (#171) 2024-04-28 13:20:09 +08:00
Dream Hunter
c73c86e86c Update CHANGELOG.md 2024-04-27 23:19:55 +08:00
12 changed files with 461 additions and 407 deletions

View File

@@ -1,5 +1,30 @@
# CHANGE LOG # CHANGE LOG
## v0.3.0
Breaking Changes:
DB changes:
`address` 表的前缀将从代码中迁移到 db 中,请将下面 sql 中的 `tmp` 替换为你的前缀,然后执行。
```sql
update
address
set
name = 'tmp' || name;
```
## v0.2.10
- `ENABLE_USER_DELETE_EMAIL` 是否允许用户删除账户和邮件
- `ENABLE_AUTO_REPLY` 是否启用自动回复
- fetchAddressError 提示改进
- 自动刷新显示倒计时
* feat: docs update by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/165
* feat: add ENABLE_USER_DELETE_EMAIL && ENABLE_AUTO_REPLY && modify fetchAddressError i18n && UI: show autoRefreshInterval by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/169
## v0.2.9 ## v0.2.9
- 添加富文本编辑器 - 添加富文本编辑器

View File

@@ -3,7 +3,6 @@ import { watch, onMounted, ref } from "vue";
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../store' import { useGlobalState } from '../store'
import { api } from '../api'
import { CloudDownloadRound } from '@vicons/material' import { CloudDownloadRound } from '@vicons/material'
import { useIsMobile } from '../utils/composables' import { useIsMobile } from '../utils/composables'
import { processItem, getDownloadEmlUrl } from '../utils/email-parser' import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
@@ -11,7 +10,30 @@ import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
const message = useMessage() const message = useMessage()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const { settings, openSettings, themeSwitch } = useGlobalState() const props = defineProps({
enableUserDeleteEmail: {
type: Boolean,
default: false,
requried: false
},
showEMailTo: {
type: Boolean,
default: true,
requried: false
},
fetchMailData: {
type: Function,
default: () => { },
requried: true
},
deleteMail: {
type: Function,
default: () => { },
requried: false
},
})
const { themeSwitch } = useGlobalState()
const autoRefresh = ref(false) const autoRefresh = ref(false)
const autoRefreshInterval = ref(30) const autoRefreshInterval = ref(30)
const data = ref([]) const data = ref([])
@@ -29,6 +51,7 @@ const { t } = useI18n({
locale: 'zh', locale: 'zh',
messages: { messages: {
en: { en: {
success: 'Success',
autoRefresh: 'Auto Refresh', autoRefresh: 'Auto Refresh',
refreshAfter: 'Refresh After {msg} Seconds', refreshAfter: 'Refresh After {msg} Seconds',
refresh: 'Refresh', refresh: 'Refresh',
@@ -39,6 +62,7 @@ const { t } = useI18n({
deleteMailTip: 'Are you sure you want to delete this mail?' deleteMailTip: 'Are you sure you want to delete this mail?'
}, },
zh: { zh: {
success: '成功',
autoRefresh: '自动刷新', autoRefresh: '自动刷新',
refreshAfter: '{msg}秒后刷新', refreshAfter: '{msg}秒后刷新',
refresh: '刷新', refresh: '刷新',
@@ -80,10 +104,8 @@ watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
const refresh = async () => { const refresh = async () => {
try { try {
const { results, count: totalCount } = await api.fetch( const { results, count: totalCount } = await props.fetchMailData(
`/api/mails` pageSize.value, (page.value - 1) * pageSize.value
+ `?limit=${pageSize.value}`
+ `&offset=${(page.value - 1) * pageSize.value}`
); );
data.value = await Promise.all(results.map(async (item) => { data.value = await Promise.all(results.map(async (item) => {
return await processItem(item); return await processItem(item);
@@ -115,9 +137,7 @@ const mailItemClass = (row) => {
const deleteMail = async () => { const deleteMail = async () => {
try { try {
await api.fetch(`/api/mails/${curMail.value.id}`, { await props.deleteMail(curMail.value.id);
method: 'DELETE'
});
message.success(t("success")); message.success(t("success"));
curMail.value = null; curMail.value = null;
await refresh(); await refresh();
@@ -133,7 +153,6 @@ onMounted(async () => {
<template> <template>
<div> <div>
<n-layout v-if="settings.address">
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.3"> <n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.3">
<template #1> <template #1>
<div class="center"> <div class="center">
@@ -164,9 +183,12 @@ onMounted(async () => {
<n-tag type="info"> <n-tag type="info">
{{ row.created_at }} {{ row.created_at }}
</n-tag> </n-tag>
<div style="word-break: break-all; font-size: small;"> <n-tag type="info">
FROM: {{ row.source }} FROM: {{ row.source }}
</div> </n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ row.address }}
</n-tag>
</template> </template>
</n-thing> </n-thing>
</n-list-item> </n-list-item>
@@ -185,7 +207,10 @@ onMounted(async () => {
<n-tag type="info"> <n-tag type="info">
FROM: {{ curMail.source }} FROM: {{ curMail.source }}
</n-tag> </n-tag>
<n-popconfirm v-if="openSettings.enableUserDeleteEmail" @positive-click="deleteMail"> <n-tag v-if="showEMailTo" type="info">
TO: {{ curMail.address }}
</n-tag>
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
<template #trigger> <template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button> <n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template> </template>
@@ -202,6 +227,7 @@ onMounted(async () => {
</n-button> </n-button>
</n-space> </n-space>
<div v-html="curMail.message" style="margin-top: 10px;"></div> <div v-html="curMail.message" style="margin-top: 10px;"></div>
<!-- <iframe :srcdoc="curMail.message" style="width: 100%; height: 100%;"></iframe> -->
</n-card> </n-card>
<n-card class="mail-item" v-else> <n-card class="mail-item" v-else>
<n-result status="info" :title="t('pleaseSelectMail')"> <n-result status="info" :title="t('pleaseSelectMail')">
@@ -236,16 +262,19 @@ onMounted(async () => {
<n-tag type="info"> <n-tag type="info">
{{ row.created_at }} {{ row.created_at }}
</n-tag> </n-tag>
<div style="word-break: break-all; font-size: small;"> <n-tag type="info">
FROM: {{ row.source }} FROM: {{ row.source }}
</div> </n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ row.address }}
</n-tag>
</template> </template>
</n-thing> </n-thing>
</n-list-item> </n-list-item>
</n-list> </n-list>
</div> </div>
<n-drawer v-model:show="curMail" width="100%" :trap-focus="false" :block-scroll="false" to="#drawer-target"> <n-drawer v-model:show="curMail" width="100%" :trap-focus="false" :block-scroll="false" to="#drawer-target">
<n-drawer-content :title="curMail.subject" closable> <n-drawer-content :title="curMail ? curMail.subject : ''" closable>
<n-card style="overflow: auto;"> <n-card style="overflow: auto;">
<n-space> <n-space>
<n-tag type="info"> <n-tag type="info">
@@ -257,7 +286,10 @@ onMounted(async () => {
<n-tag type="info"> <n-tag type="info">
FROM: {{ curMail.source }} FROM: {{ curMail.source }}
</n-tag> </n-tag>
<n-popconfirm @positive-click="deleteMail"> <n-tag v-if="showEMailTo" type="info">
TO: {{ curMail.address }}
</n-tag>
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
<template #trigger> <template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button> <n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template> </template>
@@ -278,7 +310,6 @@ onMounted(async () => {
</n-drawer-content> </n-drawer-content>
</n-drawer> </n-drawer>
</div> </div>
</n-layout>
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog"> <n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
<template #header> <template #header>
<div>{{ t("attachments") }}</div> <div>{{ t("attachments") }}</div>
@@ -302,8 +333,6 @@ onMounted(async () => {
</template> </template>
</n-list-item> </n-list-item>
</n-list> </n-list>
<template #action>
</template>
</n-modal> </n-modal>
</div> </div>
</template> </template>

View File

@@ -1,11 +1,22 @@
<script setup> <script setup>
import MailBox from './MailBox.vue'; import MailBox from '../components/MailBox.vue';
import { useGlobalState } from '../store' import { useGlobalState } from '../store'
const { settings } = useGlobalState() import { api } from '../api'
const { settings, openSettings } = useGlobalState()
const fetchMailData = async (limit, offset) => {
return await api.fetch(`/api/mails?limit=${limit}&offset=${offset}`);
};
const deleteMail = async (curMailId) => {
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
};
</script> </script>
<template> <template>
<div> <div v-if="settings.address">
<MailBox v-if="settings.address" /> <MailBox :showEMailTo="false" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
</div> </div>
</template> </template>

View File

@@ -1,10 +1,11 @@
<script setup> <script setup>
import { ref, h, onMounted, watch } from 'vue'; import { ref, h, onMounted, watch } from 'vue';
import { NBadge } from 'naive-ui'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
import { api } from '../../api' import { api } from '../../api'
import { NMenu } from 'naive-ui'; import { NButton, NMenu } from 'naive-ui';
import { MenuFilled } from '@vicons/material' import { MenuFilled } from '@vicons/material'
const { const {
@@ -19,6 +20,9 @@ const { t } = useI18n({
en: { en: {
name: 'Name', name: 'Name',
created_at: 'Created At', created_at: 'Created At',
updated_at: 'Update At',
mail_count: 'Mail Count',
send_count: 'Send Count',
showPass: 'Show Passwrod', showPass: 'Show Passwrod',
password: 'Password', password: 'Password',
passwordTip: 'Please copy the password and you can use it to login to your email account.', passwordTip: 'Please copy the password and you can use it to login to your email account.',
@@ -35,6 +39,9 @@ const { t } = useI18n({
zh: { zh: {
name: '名称', name: '名称',
created_at: '创建时间', created_at: '创建时间',
updated_at: '更新时间',
mail_count: '邮件数量',
send_count: '发送数量',
showPass: '显示密码', showPass: '显示密码',
password: '密码', password: '密码',
passwordTip: '请复制密码,你可以使用它登录你的邮箱。', passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
@@ -116,6 +123,62 @@ const columns = [
title: t('created_at'), title: t('created_at'),
key: "created_at" key: "created_at"
}, },
{
title: t('updated_at'),
key: "updated_at"
},
{
title: t('mail_count'),
key: "mail_count",
render(row) {
return h(NButton,
{
text: true,
onClick: () => {
if (row.mail_count > 0) {
adminMailTabAddress.value = row.name;
adminTab.value = "mails";
}
}
},
{
icon: () => h(NBadge, {
value: row.mail_count,
'show-zero': true,
max: 99,
type: "success"
}),
default: () => row.mail_count > 0 ? t('viewMails') : ""
}
)
}
},
{
title: t('send_count'),
key: "send_count",
render(row) {
return h(NButton,
{
text: true,
onClick: () => {
if (row.send_count > 0) {
adminSendBoxTabAddress.value = row.name;
adminTab.value = "sendBox";
}
}
},
{
icon: () => h(NBadge, {
value: row.send_count,
'show-zero': true,
max: 99,
type: "success"
}),
default: () => row.send_count > 0 ? t('viewSendBox') : ""
}
)
}
},
{ {
title: t('actions'), title: t('actions'),
key: 'actions', key: 'actions',
@@ -132,8 +195,7 @@ const columns = [
{ {
label: () => h(NButton, label: () => h(NButton,
{ {
bordered: false, text: true,
ghost: true,
onClick: () => showPassword(row.id) onClick: () => showPassword(row.id)
}, },
{ default: () => t('showPass') } { default: () => t('showPass') }
@@ -142,8 +204,7 @@ const columns = [
{ {
label: () => h(NButton, label: () => h(NButton,
{ {
bordered: false, text: true,
ghost: true,
onClick: () => { onClick: () => {
adminMailTabAddress.value = row.name; adminMailTabAddress.value = row.name;
adminTab.value = "mails"; adminTab.value = "mails";
@@ -155,8 +216,7 @@ const columns = [
{ {
label: () => h(NButton, label: () => h(NButton,
{ {
bordered: false, text: true,
ghost: true,
onClick: () => { onClick: () => {
adminSendBoxTabAddress.value = row.name; adminSendBoxTabAddress.value = row.name;
adminTab.value = "sendBox"; adminTab.value = "sendBox";
@@ -168,8 +228,7 @@ const columns = [
{ {
label: () => h(NButton, label: () => h(NButton,
{ {
bordered: false, text: true,
ghost: true,
onClick: () => { onClick: () => {
curDeleteAddressId.value = row.id; curDeleteAddressId.value = row.id;
showDelteAccount.value = true; showDelteAccount.value = true;

View File

@@ -1,63 +1,43 @@
<script setup> <script setup>
import { ref, onMounted, watch } from 'vue'; import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
import { api } from '../../api' import { api } from '../../api'
import { processItem } from '../../utils/email-parser' import MailBox from '../../components/MailBox.vue';
const { const {
localeCache, adminAuth, showAdminAuth, localeCache, adminAuth, showAdminAuth,
adminMailTabAddress adminMailTabAddress
} = useGlobalState() } = useGlobalState()
const message = useMessage()
const { t } = useI18n({ const { t } = useI18n({
locale: localeCache.value || 'zh', locale: localeCache.value || 'zh',
messages: { messages: {
en: { en: {
mails: 'Emails', addressQueryTip: 'Leave blank to query all addresses',
itemCount: 'itemCount',
query: 'Query', query: 'Query',
}, },
zh: { zh: {
mails: '邮件', addressQueryTip: '留空查询所有地址',
itemCount: '总数',
query: '查询', query: '查询',
} }
} }
}); });
const mailData = ref([]) const mailBoxKey = ref("")
const mailCount = ref(0)
const mailPage = ref(1)
const mailPageSize = ref(20)
watch([mailPage, mailPageSize, adminMailTabAddress], async () => { const queryAddress = () => {
await fetchMailData() mailBoxKey.value = adminMailTabAddress.value;
}) }
const fetchMailData = async () => { const fetchMailData = async (limit, offset) => {
if (!adminMailTabAddress.value) { return await api.fetch(
return
}
try {
const { results, count } = await api.fetch(
`/admin/mails` `/admin/mails`
+ `?address=${adminMailTabAddress.value}` + `?limit=${limit}`
+ `&limit=${mailPageSize.value}` + `&offset=${offset}`
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}` + (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '')
); );
mailData.value = await Promise.all(results.map(async (item) => {
return await processItem(item);
}));
if (count > 0) {
mailCount.value = count;
}
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
} }
onMounted(async () => { onMounted(async () => {
@@ -65,48 +45,17 @@ onMounted(async () => {
showAdminAuth.value = true; showAdminAuth.value = true;
return; return;
} }
await fetchMailData()
}) })
</script> </script>
<template> <template>
<div> <div>
<n-input-group> <n-input-group>
<n-input v-model:value="adminMailTabAddress" /> <n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
<n-button @click="fetchMailData" type="primary" ghost> <n-button @click="queryAddress" type="primary" ghost>
{{ t('query') }} {{ t('query') }}
</n-button> </n-button>
</n-input-group> </n-input-group>
<n-list hoverable clickable> <MailBox :key="mailBoxKey" :enableUserDeleteEmail="false" :fetchMailData="fetchMailData" />
<div style="display: inline-block; margin-bottom: 10px;">
<n-pagination v-model:page="mailPage" v-model:page-size="mailPageSize" :item-count="mailCount" simple>
<template #prefix="{ itemCount }">
{{ t('itemCount') }}: {{ itemCount }}
</template>
</n-pagination>
</div>
<n-list-item v-for="row in mailData" v-bind:key="row.id">
<n-thing class="center" :title="row.subject">
<template #description>
<n-space>
<n-tag type="info">
FROM: {{ row.source }}
</n-tag>
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
</n-space>
</template>
<div v-html="row.message"></div>
</n-thing>
</n-list-item>
</n-list>
</div> </div>
</template> </template>
<style scoped>
.n-pagination {
margin-top: 10px;
margin-bottom: 10px;
}
</style>

View File

@@ -1,56 +1,18 @@
<script setup> <script setup>
import { ref, onMounted, watch } from 'vue'; import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
import { api } from '../../api' import { api } from '../../api'
import { processItem } from '../../utils/email-parser' import MailBox from '../../components/MailBox.vue';
const { const { adminAuth, showAdminAuth } = useGlobalState()
localeCache, adminAuth, showAdminAuth
} = useGlobalState()
const message = useMessage()
const { t } = useI18n({ const fetchMailUnknowData = async (limit, offset) => {
locale: localeCache.value || 'zh', return await api.fetch(
messages: {
en: {
itemCount: 'itemCount',
refresh: 'Refresh'
},
zh: {
itemCount: '总数',
refresh: '刷新'
}
}
});
const mailUnknowData = ref([])
const mailUnknowCount = ref(0)
const mailUnknowPage = ref(1)
const mailUnknowPageSize = ref(20)
watch([mailUnknowPage, mailUnknowPageSize], async () => {
await fetchMailUnknowData()
})
const fetchMailUnknowData = async () => {
try {
const { results, count } = await api.fetch(
`/admin/mails_unknow` `/admin/mails_unknow`
+ `?limit=${mailUnknowPageSize.value}` + `?limit=${limit}`
+ `&offset=${(mailUnknowPage.value - 1) * mailUnknowPage.value}` + `&offset=${offset}`
); );
mailUnknowData.value = await Promise.all(results.map(async (item) => {
return await processItem(item);
}));
if (count > 0) {
mailUnknowCount.value = count;
}
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
} }
onMounted(async () => { onMounted(async () => {
@@ -58,49 +20,11 @@ onMounted(async () => {
showAdminAuth.value = true; showAdminAuth.value = true;
return; return;
} }
await fetchMailUnknowData();
}) })
</script> </script>
<template> <template>
<div> <div v-if="adminAuth">
<n-button @click="fetchMailUnknowData" type="primary" ghost> <MailBox :enableUserDeleteEmail="false" :fetchMailData="fetchMailUnknowData" />
{{ t('refresh') }}
</n-button>
<n-list hoverable clickable>
<div style="display: inline-block; margin-bottom: 10px;">
<n-pagination v-model:page="mailUnknowPage" v-model:page-size="mailUnknowPageSize"
:item-count="mailUnknowCount" simple>
<template #prefix="{ itemCount }">
{{ t('itemCount') }}: {{ itemCount }}
</template>
</n-pagination>
</div>
<n-list-item v-for="row in mailUnknowData" v-bind:key="row.id">
<n-thing class="center" :title="row.subject">
<template #description>
<n-space>
<n-tag type="info">
FROM: {{ row.source }}
</n-tag>
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
<n-tag type="info">
TO: {{ row.address }}
</n-tag>
</n-space>
</template>
<div v-html="row.message"></div>
</n-thing>
</n-list-item>
</n-list>
</div> </div>
</template> </template>
<style scoped>
.n-pagination {
margin-top: 10px;
margin-bottom: 10px;
}
</style>

View File

@@ -44,15 +44,12 @@ const curRow = ref({})
const showModal = ref(false) const showModal = ref(false)
const fetchData = async () => { const fetchData = async () => {
if (!adminSendBoxTabAddress.value) {
return
}
try { try {
const { results, count: addressCount } = await api.fetch( const { results, count: addressCount } = await api.fetch(
`/admin/sendbox` `/admin/sendbox`
+ `?address=${adminSendBoxTabAddress.value}` + `?limit=${pageSize.value}`
+ `&limit=${pageSize.value}`
+ `&offset=${(page.value - 1) * pageSize.value}` + `&offset=${(page.value - 1) * pageSize.value}`
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
); );
data.value = results.map((item) => { data.value = results.map((item) => {
try { try {

View File

@@ -22,7 +22,7 @@ const { t } = useI18n({
itemCount: 'itemCount', itemCount: 'itemCount',
modalTip: 'Please input the sender balance', modalTip: 'Please input the sender balance',
balance: 'Balance', balance: 'Balance',
refresh: 'Refresh', query: 'Query',
ok: 'OK' ok: 'OK'
}, },
zh: { zh: {
@@ -36,7 +36,7 @@ const { t } = useI18n({
itemCount: '总数', itemCount: '总数',
modalTip: '请输入发件额度', modalTip: '请输入发件额度',
balance: '余额', balance: '余额',
refresh: '刷新', query: '查询',
ok: '确定' ok: '确定'
} }
} }
@@ -51,6 +51,7 @@ const showModal = ref(false)
const senderBalance = ref(0) const senderBalance = ref(0)
const senderEnabled = ref(false) const senderEnabled = ref(false)
const addressQuery = ref('')
const updateData = async () => { const updateData = async () => {
try { try {
@@ -77,6 +78,7 @@ const fetchData = async () => {
`/admin/address_sender` `/admin/address_sender`
+ `?limit=${pageSize.value}` + `?limit=${pageSize.value}`
+ `&offset=${(page.value - 1) * pageSize.value}` + `&offset=${(page.value - 1) * pageSize.value}`
+ (addressQuery.value ? `&address=${addressQuery.value}` : '')
); );
data.value = results; data.value = results;
if (addressCount > 0) { if (addressCount > 0) {
@@ -166,17 +168,18 @@ onMounted(async () => {
</n-button> </n-button>
</template> </template>
</n-modal> </n-modal>
<n-input-group>
<n-input v-model:value="addressQuery" />
<n-button @click="fetchData" type="primary" ghost>
{{ t('query') }}
</n-button>
</n-input-group>
<div style="display: inline-block;"> <div style="display: inline-block;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]" <n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
show-size-picker> show-size-picker>
<template #prefix="{ itemCount }"> <template #prefix="{ itemCount }">
{{ t('itemCount') }}: {{ itemCount }} {{ t('itemCount') }}: {{ itemCount }}
</template> </template>
<template #suffix>
<n-button @click="fetchData" type="primary" size="small" ghost>
{{ t('refresh') }}
</n-button>
</template>
</n-pagination> </n-pagination>
</div> </div>
<n-data-table :columns="columns" :data="data" :bordered="false" /> <n-data-table :columns="columns" :data="data" :bordered="false" />

View File

@@ -15,25 +15,31 @@ api.get('/admin/address', async (c) => {
} }
if (query) { if (query) {
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT * FROM address where concat('${c.env.PREFIX}', name) like ? order by id desc limit ? offset ? ` `SELECT a.*,`
+ ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
+ ` FROM address a`
+ ` where name like ?`
+ ` order by id desc limit ? offset ?`
).bind(`%${query}%`, limit, offset).all(); ).bind(`%${query}%`, limit, offset).all();
let count = 0; let count = 0;
if (offset == 0) { if (offset == 0) {
const { count: addressCount } = await c.env.DB.prepare( const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address where concat('${c.env.PREFIX}', name) like ?` `SELECT count(*) as count FROM address where name like ?`
).bind(`%${query}%`).first(); ).bind(`%${query}%`).first();
count = addressCount; count = addressCount;
} }
return c.json({ return c.json({
results: results.map((r) => { results: results,
r.name = c.env.PREFIX + r.name;
return r;
}),
count: count count: count
}) })
} }
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT * FROM address order by id desc limit ? offset ? ` `SELECT a.*,`
+ ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
+ ` FROM address a`
+ ` order by id desc limit ? offset ?`
).bind(limit, offset).all(); ).bind(limit, offset).all();
let count = 0; let count = 0;
if (offset == 0) { if (offset == 0) {
@@ -43,10 +49,7 @@ api.get('/admin/address', async (c) => {
count = addressCount; count = addressCount;
} }
return c.json({ return c.json({
results: results.map((r) => { results: results,
r.name = c.env.PREFIX + r.name;
return r;
}),
count: count count: count
}) })
}) })
@@ -61,7 +64,7 @@ api.delete('/admin/delete_address/:id', async (c) => {
} }
const { success: mailSuccess } = await c.env.DB.prepare( const { success: mailSuccess } = await c.env.DB.prepare(
`DELETE FROM mails WHERE address IN `DELETE FROM mails WHERE address IN
(select concat('${c.env.PREFIX}', name) from address where id = ?) ` (select name from address where id = ?) `
).bind(id).run(); ).bind(id).run();
if (!mailSuccess) { if (!mailSuccess) {
return c.text("Failed to delete mails", 500) return c.text("Failed to delete mails", 500)
@@ -79,10 +82,8 @@ api.get('/admin/show_password/:id', async (c) => {
const name = await c.env.DB.prepare( const name = await c.env.DB.prepare(
`SELECT name FROM address WHERE id = ? ` `SELECT name FROM address WHERE id = ? `
).bind(id).first("name"); ).bind(id).first("name");
// compute address
const emailAddress = c.env.PREFIX + name
const jwt = await Jwt.sign({ const jwt = await Jwt.sign({
address: emailAddress, address: name,
address_id: id address_id: id
}, c.env.JWT_SECRET) }, c.env.JWT_SECRET)
return c.json({ return c.json({
@@ -99,6 +100,22 @@ api.get('/admin/mails', async (c) => {
if (!offset || offset < 0) { if (!offset || offset < 0) {
return c.text("Invalid offset", 400) return c.text("Invalid offset", 400)
} }
if (!address) {
const { results } = await c.env.DB.prepare(
`SELECT * FROM raw_mails order by id desc limit ? offset ?`
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM raw_mails`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
}
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT * FROM raw_mails where address = ? order by id desc limit ? offset ?` `SELECT * FROM raw_mails where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all(); ).bind(address, limit, offset).all();
@@ -125,7 +142,7 @@ api.get('/admin/mails_unknow', async (c) => {
} }
const { results } = await c.env.DB.prepare(` const { results } = await c.env.DB.prepare(`
SELECT * FROM raw_mails SELECT * FROM raw_mails
where address NOT IN(select concat('${c.env.PREFIX}', name) from address) where address NOT IN (select name from address)
order by id desc limit ? offset ? ` order by id desc limit ? offset ? `
).bind(limit, offset).all(); ).bind(limit, offset).all();
let count = 0; let count = 0;
@@ -133,7 +150,7 @@ api.get('/admin/mails_unknow', async (c) => {
const { count: mailCount } = await c.env.DB.prepare(` const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM raw_mails SELECT count(*) as count FROM raw_mails
where address NOT IN where address NOT IN
(select concat('${c.env.PREFIX}', name) from address)` (select name from address)`
).first(); ).first();
count = mailCount; count = mailCount;
} }
@@ -144,13 +161,29 @@ api.get('/admin/mails_unknow', async (c) => {
}); });
api.get('/admin/address_sender', async (c) => { api.get('/admin/address_sender', async (c) => {
const { limit, offset } = c.req.query(); const { address, limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) { if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400) return c.text("Invalid limit", 400)
} }
if (!offset || offset < 0) { if (!offset || offset < 0) {
return c.text("Invalid offset", 400) return c.text("Invalid offset", 400)
} }
if (address) {
const { results } = await c.env.DB.prepare(
`SELECT * FROM address_sender where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address_sender where address = ?`
).bind(address).first();
count = addressCount;
}
return c.json({
results: results,
count: count
})
}
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT * FROM address_sender order by id desc limit ? offset ? ` `SELECT * FROM address_sender order by id desc limit ? offset ? `
).bind(limit, offset).all(); ).bind(limit, offset).all();
@@ -190,7 +223,42 @@ api.post('/admin/address_sender', async (c) => {
api.get('/admin/sendbox', async (c) => { api.get('/admin/sendbox', async (c) => {
const { address, limit, offset } = c.req.query(); const { address, limit, offset } = c.req.query();
return getSendbox(c, address, limit, offset); if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
if (!address) {
const { results } = await c.env.DB.prepare(
`SELECT * FROM sendbox order by id desc limit ? offset ?`
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM sendbox`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
}
const { results } = await c.env.DB.prepare(
`SELECT * FROM sendbox where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM sendbox where address = ?`
).bind(address).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
}) })
api.get('/admin/statistics', async (c) => { api.get('/admin/statistics', async (c) => {
@@ -232,7 +300,7 @@ api.post('/admin/cleanup', async (c) => {
case "mails_unknow": case "mails_unknow":
await c.env.DB.prepare(` await c.env.DB.prepare(`
DELETE FROM raw_mails WHERE address NOT IN DELETE FROM raw_mails WHERE address NOT IN
(select concat('${c.env.PREFIX}', name) from address) AND created_at < datetime('now', '-${cleanDays} day')` (select name from address) AND created_at < datetime('now', '-${cleanDays} day')`
).run(); ).run();
break; break;
case "address": case "address":

View File

@@ -37,15 +37,14 @@ api.get('/admin/v1/mails_unknow', async (c) => {
} }
const { results } = await c.env.DB.prepare(` const { results } = await c.env.DB.prepare(`
SELECT id, source, subject, message FROM mails SELECT id, source, subject, message FROM mails
where address NOT IN(select concat('${c.env.PREFIX}', name) from address) where address NOT IN(select name from address)
order by id desc limit ? offset ? ` order by id desc limit ? offset ? `
).bind(limit, offset).all(); ).bind(limit, offset).all();
let count = 0; let count = 0;
if (offset == 0) { if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(` const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM mails SELECT count(*) as count FROM mails
where address NOT IN where address NOT IN (select name from address)`
(select concat('${c.env.PREFIX}', name) from address)`
).first(); ).first();
count = mailCount; count = mailCount;
} }

View File

@@ -7,7 +7,6 @@ async function email(message, env, ctx) {
console.log(`Reject message from ${message.from} to ${message.to}`); console.log(`Reject message from ${message.from} to ${message.to}`);
return; return;
} }
if (!env.PREFIX || (message.to && message.to.startsWith(env.PREFIX))) {
const rawEmail = await new Response(message.raw).text(); const rawEmail = await new Response(message.raw).text();
const message_id = message.headers.get("Message-ID"); const message_id = message.headers.get("Message-ID");
// save email // save email
@@ -52,10 +51,6 @@ async function email(message, env, ctx) {
console.log("reply email error", error); console.log("reply email error", error);
} }
} }
} else {
message.setReject(`Unknown address ${message.to}`);
console.log(`Unknown address ${message.to}`);
}
} }
export { email } export { email }

View File

@@ -18,7 +18,7 @@ api.get('/api/mails', async (c) => {
return c.text("Invalid offset", 400) return c.text("Invalid offset", 400)
} }
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT id, source, raw, created_at FROM raw_mails where address = ? order by id desc limit ? offset ?` `SELECT * FROM raw_mails where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all(); ).bind(address, limit, offset).all();
let count = 0; let count = 0;
if (offset == 0) { if (offset == 0) {
@@ -34,7 +34,7 @@ api.get('/api/mails', async (c) => {
}) })
api.delete('/api/mails/:id', async (c) => { api.delete('/api/mails/:id', async (c) => {
if (c.env.ENABLE_USER_DELETE_EMAIL) { if (!c.env.ENABLE_USER_DELETE_EMAIL) {
return c.text("User delete email is disabled", 403) return c.text("User delete email is disabled", 403)
} }
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
@@ -61,13 +61,12 @@ api.get('/api/settings', async (c) => {
return c.text("Invalid address", 400) return c.text("Invalid address", 400)
} }
} }
if (address.startsWith(c.env.PREFIX)) {
// check address id // check address id
try { try {
if (!address_id) { if (!address_id) {
const db_address_id = await c.env.DB.prepare( const db_address_id = await c.env.DB.prepare(
`SELECT id FROM address where name = ?` `SELECT id FROM address where name = ?`
).bind(address.substring(c.env.PREFIX.length)).first("id"); ).bind(address).first("id");
if (!db_address_id) { if (!db_address_id) {
return c.text("Invalid address", 400) return c.text("Invalid address", 400)
} }
@@ -79,11 +78,10 @@ api.get('/api/settings', async (c) => {
try { try {
c.env.DB.prepare( c.env.DB.prepare(
`UPDATE address SET updated_at = datetime('now') where name = ?` `UPDATE address SET updated_at = datetime('now') where name = ?`
).bind(address.substring(c.env.PREFIX.length)).run(); ).bind(address).run();
} catch (e) { } catch (e) {
console.warn("Failed to update address") console.warn("Failed to update address")
} }
}
let auto_reply = {}; let auto_reply = {};
if (c.env.ENABLE_AUTO_REPLY) { if (c.env.ENABLE_AUTO_REPLY) {
const results = await c.env.DB.prepare( const results = await c.env.DB.prepare(
@@ -181,11 +179,11 @@ api.get('/api/new_address', async (c) => {
domain = domains[Math.floor(Math.random() * domains.length)]; domain = domains[Math.floor(Math.random() * domains.length)];
} }
// create address // create address
const emailAddress = c.env.PREFIX + name + "@" + domain name = c.env.PREFIX + name + "@" + domain
try { try {
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
`INSERT INTO address(name) VALUES(?)` `INSERT INTO address(name) VALUES(?)`
).bind(name + "@" + domain).run(); ).bind(name).run();
if (!success) { if (!success) {
return c.text("Failed to create address", 500) return c.text("Failed to create address", 500)
} }
@@ -199,13 +197,13 @@ api.get('/api/new_address', async (c) => {
try { try {
address_id = await c.env.DB.prepare( address_id = await c.env.DB.prepare(
`SELECT id FROM address where name = ?` `SELECT id FROM address where name = ?`
).bind(name + "@" + domain).first("id"); ).bind(name).first("id");
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
// create jwt // create jwt
const jwt = await Jwt.sign({ const jwt = await Jwt.sign({
address: emailAddress, address: name,
address_id: address_id address_id: address_id
}, c.env.JWT_SECRET) }, c.env.JWT_SECRET)
return c.json({ return c.json({
@@ -214,14 +212,11 @@ api.get('/api/new_address', async (c) => {
}) })
api.delete('/api/delete_address', async (c) => { api.delete('/api/delete_address', async (c) => {
if (c.env.ENABLE_USER_DELETE_EMAIL) { if (!c.env.ENABLE_USER_DELETE_EMAIL) {
return c.text("User delete email is disabled", 403) return c.text("User delete email is disabled", 403)
} }
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
let name = address; let name = address;
if (address.startsWith(c.env.PREFIX)) {
name = address.substring(c.env.PREFIX.length);
}
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
`DELETE FROM address WHERE name = ? ` `DELETE FROM address WHERE name = ? `
).bind(name).run(); ).bind(name).run();