mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-07 00:17:12 +08:00
feat: SMTP IMAP Proxy: add sendbox && UI: sendbox use split view (#248)
This commit is contained in:
@@ -12,6 +12,8 @@
|
|||||||
- `ENABLE_WEBHOOK` 添加 webhook
|
- `ENABLE_WEBHOOK` 添加 webhook
|
||||||
- UI: admin 页面使用双层 tab
|
- UI: admin 页面使用双层 tab
|
||||||
- UI: 登录后可直接主页切换地址
|
- UI: 登录后可直接主页切换地址
|
||||||
|
- UI: 发件箱也采用左右分栏显示(类似收件箱)
|
||||||
|
- `SMTP IMAP Proxy` 添加发件箱查看
|
||||||
|
|
||||||
## v0.4.2
|
## v0.4.2
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ onBeforeUnmount(() => {
|
|||||||
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
|
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
|
||||||
<template #1>
|
<template #1>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
<div style="display: inline-block;">
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
</div>
|
</div>
|
||||||
<n-switch v-model:value="autoRefresh" size="small" :round="false">
|
<n-switch v-model:value="autoRefresh" size="small" :round="false">
|
||||||
@@ -296,7 +296,7 @@ onBeforeUnmount(() => {
|
|||||||
</n-split>
|
</n-split>
|
||||||
<div class="left" v-else>
|
<div class="left" v-else>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
<div style="display: inline-block;">
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
</div>
|
</div>
|
||||||
<n-switch v-model:value="autoRefresh" size="small" :round="false">
|
<n-switch v-model:value="autoRefresh" size="small" :round="false">
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
<script setup>
|
||||||
|
import { watch, onMounted, ref } from "vue";
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useGlobalState } from '../store'
|
||||||
|
import { useIsMobile } from '../utils/composables'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
showEMailFrom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
fetchMailData: {
|
||||||
|
type: Function,
|
||||||
|
default: () => { },
|
||||||
|
requried: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { localeCache, isDark, mailboxSplitSize } = useGlobalState()
|
||||||
|
const data = ref([])
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
|
||||||
|
const curMail = ref(null);
|
||||||
|
const showCode = ref(false)
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
success: 'Success',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
showCode: 'Change View Original Code',
|
||||||
|
pleaseSelectMail: "Please select a mail to view."
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
success: '成功',
|
||||||
|
refresh: '刷新',
|
||||||
|
showCode: '切换查看元数据',
|
||||||
|
pleaseSelectMail: "请选择一封邮件查看。",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
||||||
|
if (page !== oldPage || pageSize !== oldPageSize) {
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
try {
|
||||||
|
const { results, count: totalCount } = await props.fetchMailData(
|
||||||
|
pageSize.value, (page.value - 1) * pageSize.value
|
||||||
|
);
|
||||||
|
data.value = results.map((item) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(item.raw);
|
||||||
|
item.to_mail = data?.personalizations?.map(
|
||||||
|
(p) => p.to?.map((t) => t.email).join(',')
|
||||||
|
).join(';');
|
||||||
|
item.subject = data.subject;
|
||||||
|
item.contentType = data.content[0]?.type;
|
||||||
|
item.content = data.content[0]?.value;
|
||||||
|
item.raw = JSON.stringify(data, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
if (totalCount > 0) {
|
||||||
|
count.value = totalCount;
|
||||||
|
}
|
||||||
|
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
||||||
|
curMail.value = data.value[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickRow = async (row) => {
|
||||||
|
curMail.value = row;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mailItemClass = (row) => {
|
||||||
|
return curMail.value && row.id == curMail.value.id ? (isDark.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSpiltSizeChange = (size) => {
|
||||||
|
mailboxSplitSize.value = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25"
|
||||||
|
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
|
||||||
|
<template #1>
|
||||||
|
<div class="center">
|
||||||
|
<div style="display: inline-block; margin-right: 10px;">
|
||||||
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
|
</div>
|
||||||
|
<n-button @click="refresh" size="small" type="primary" tertiary>
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div style="overflow: auto; height: 80vh;">
|
||||||
|
<n-list hoverable clickable>
|
||||||
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||||
|
:class="mailItemClass(row)">
|
||||||
|
<n-thing :title="row.subject">
|
||||||
|
<template #description>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ row.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ row.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="showEMailFrom" type="info">
|
||||||
|
FROM: {{ row.address }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
TO: {{ row.to_mail }}
|
||||||
|
</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #2>
|
||||||
|
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ curMail.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ curMail.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ curMail.address }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
TO: {{ curMail.to_mail }}
|
||||||
|
</n-tag>
|
||||||
|
<n-button size="small" tertiary type="info" @click="showCode = !showCode">
|
||||||
|
{{ t('showCode') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
||||||
|
<pre v-else-if="curMail.contentType == 'text/plain'" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
||||||
|
<div v-else v-html="curMail.content" style="margin-top: 10px;"></div>
|
||||||
|
</n-card>
|
||||||
|
<n-card class="mail-item" v-else>
|
||||||
|
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||||
|
</n-result>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
</n-split>
|
||||||
|
<div class="left" v-else>
|
||||||
|
<div class="center">
|
||||||
|
<div style="display: inline-block; margin-right: 10px;">
|
||||||
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
|
</div>
|
||||||
|
<n-button @click="refresh" size="small" type="primary">
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div style="overflow: auto; height: 80vh;">
|
||||||
|
<n-list hoverable clickable>
|
||||||
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
||||||
|
<n-thing :title="row.subject">
|
||||||
|
<template #description>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ row.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ row.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="showEMailFrom" type="info">
|
||||||
|
FROM: {{ row.address }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
TO: {{ row.to_mail }}
|
||||||
|
</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</div>
|
||||||
|
<n-drawer v-model:show="curMail" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||||
|
style="height: 80vh;">
|
||||||
|
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
||||||
|
<n-card style="overflow: auto;">
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ curMail.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ curMail.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ curMail.address }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
TO: {{ curMail.to_mail }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
||||||
|
<pre v-else-if="curMail.contentType == 'text/plain'" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
||||||
|
<div v-else v-html="curMail.content" style="margin-top: 10px;"></div>
|
||||||
|
</n-card>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-dark-backgroud {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-light-backgroud {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-item {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,8 +6,8 @@ import { api } from '../api'
|
|||||||
|
|
||||||
import AddressBar from './index/AddressBar.vue';
|
import AddressBar from './index/AddressBar.vue';
|
||||||
import MailBox from '../components/MailBox.vue';
|
import MailBox from '../components/MailBox.vue';
|
||||||
|
import SendBox from '../components/SendBox.vue';
|
||||||
import AutoReply from './index/AutoReply.vue';
|
import AutoReply from './index/AutoReply.vue';
|
||||||
import SendBox from './index/SendBox.vue';
|
|
||||||
import SendMail from './index/SendMail.vue';
|
import SendMail from './index/SendMail.vue';
|
||||||
import AccountSettings from './index/AccountSettings.vue';
|
import AccountSettings from './index/AccountSettings.vue';
|
||||||
import WenHook from './index/Webhook.vue';
|
import WenHook from './index/Webhook.vue';
|
||||||
@@ -44,6 +44,10 @@ const fetchMailData = async (limit, offset) => {
|
|||||||
const deleteMail = async (curMailId) => {
|
const deleteMail = async (curMailId) => {
|
||||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchSenboxData = async (limit, offset) => {
|
||||||
|
return await api.fetch(`/api/sendbox?limit=${limit}&offset=${offset}`);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -55,7 +59,7 @@ const deleteMail = async (curMailId) => {
|
|||||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||||
<SendBox />
|
<SendBox :fetchMailData="fetchSenboxData" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
||||||
<SendMail />
|
<SendMail />
|
||||||
|
|||||||
@@ -1,153 +1,43 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, h, onMounted, watch } 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 SendBox from '../../components/SendBox.vue';
|
||||||
|
|
||||||
const { localeCache, adminAuth, adminSendBoxTabAddress, showAdminAuth } = useGlobalState()
|
const { localeCache, adminSendBoxTabAddress } = useGlobalState()
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
locale: localeCache.value || 'zh',
|
locale: localeCache.value || 'zh',
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
address: 'Address',
|
|
||||||
success: 'Success',
|
|
||||||
to_mail: 'To Mail',
|
|
||||||
subject: 'Subject',
|
|
||||||
created_at: 'Created At',
|
|
||||||
action: 'Action',
|
|
||||||
query: 'Query',
|
query: 'Query',
|
||||||
itemCount: 'itemCount',
|
queryTip: 'Please input address to query, leave blank to query all',
|
||||||
view: 'View',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
address: '地址',
|
|
||||||
success: '成功',
|
|
||||||
to_mail: '收件人邮箱',
|
|
||||||
subject: '主题',
|
|
||||||
created_at: '创建时间',
|
|
||||||
action: '操作',
|
|
||||||
query: '查询',
|
query: '查询',
|
||||||
itemCount: '总数',
|
queryTip: '请输入地址查询, 留空则查询所有',
|
||||||
view: '查看',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const data = ref([])
|
|
||||||
const count = ref(0)
|
|
||||||
const page = ref(1)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
|
|
||||||
const curRow = ref({})
|
const fetchData = async (limit, offset) => {
|
||||||
const showModal = ref(false)
|
return await api.fetch(
|
||||||
|
`/admin/sendbox?limit=${limit}&offset=${offset}`
|
||||||
const fetchData = async () => {
|
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
|
||||||
try {
|
);
|
||||||
const { results, count: addressCount } = await api.fetch(
|
|
||||||
`/admin/sendbox`
|
|
||||||
+ `?limit=${pageSize.value}`
|
|
||||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
|
||||||
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
|
|
||||||
);
|
|
||||||
data.value = results.map((item) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(item.raw);
|
|
||||||
item.to_mail = data?.personalizations?.map(
|
|
||||||
(p) => p.to?.map((t) => t.email).join(',')
|
|
||||||
).join(';');
|
|
||||||
item.subject = data.subject;
|
|
||||||
item.raw = JSON.stringify(data, null, 2);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
if (addressCount > 0) {
|
|
||||||
count.value = addressCount;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('address'),
|
|
||||||
key: "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('to_mail'),
|
|
||||||
key: "to_mail"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('subject'),
|
|
||||||
key: "subject"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('created_at'),
|
|
||||||
key: "created_at"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('action'),
|
|
||||||
key: 'actions',
|
|
||||||
render(row) {
|
|
||||||
return h('div', [
|
|
||||||
h(NButton,
|
|
||||||
{
|
|
||||||
type: 'success',
|
|
||||||
tertiary: true,
|
|
||||||
onClick: () => {
|
|
||||||
showModal.value = true;
|
|
||||||
curRow.value = row;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => t('view') }
|
|
||||||
)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
watch([page, pageSize], async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!adminAuth.value) {
|
|
||||||
showAdminAuth.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-modal v-model:show="showModal" preset="dialog" style="width: 100%;">
|
|
||||||
<pre style="overflow: auto;">{{ curRow.raw }}</pre>
|
|
||||||
</n-modal>
|
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="adminSendBoxTabAddress" />
|
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
<div style="display: inline-block;">
|
<SendBox style="margin-top: 10px;" :fetchMailData="fetchData" :showEMailFrom="true" />
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
|
||||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
|
||||||
<template #prefix="{ itemCount }">
|
|
||||||
{{ t('itemCount') }}: {{ itemCount }}
|
|
||||||
</template>
|
|
||||||
</n-pagination>
|
|
||||||
</div>
|
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, h, onMounted, watch } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
|
|
||||||
const { localeCache, settings } = useGlobalState()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
address: 'Address',
|
|
||||||
success: 'Success',
|
|
||||||
to_mail: 'To Mail',
|
|
||||||
subject: 'Subject',
|
|
||||||
created_at: 'Created At',
|
|
||||||
action: 'Action',
|
|
||||||
refresh: 'Refresh',
|
|
||||||
itemCount: 'itemCount',
|
|
||||||
view: 'View',
|
|
||||||
ok: 'OK'
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
address: '地址',
|
|
||||||
success: '成功',
|
|
||||||
to_mail: '收件人邮箱',
|
|
||||||
subject: '主题',
|
|
||||||
created_at: '创建时间',
|
|
||||||
action: '操作',
|
|
||||||
refresh: '刷新',
|
|
||||||
itemCount: '总数',
|
|
||||||
view: '查看',
|
|
||||||
ok: '确定'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const data = ref([])
|
|
||||||
const count = ref(0)
|
|
||||||
const page = ref(1)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
|
|
||||||
const curRow = ref({})
|
|
||||||
const showModal = ref(false)
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const { results, count: addressCount } = await api.fetch(
|
|
||||||
`/api/sendbox`
|
|
||||||
+ `?limit=${pageSize.value}`
|
|
||||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
|
||||||
);
|
|
||||||
data.value = results.map((item) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(item.raw);
|
|
||||||
item.to_mail = data?.personalizations?.map(
|
|
||||||
(p) => p.to?.map((t) => t.email).join(',')
|
|
||||||
).join(';');
|
|
||||||
item.subject = data.subject;
|
|
||||||
item.raw = JSON.stringify(data, null, 2);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
if (addressCount > 0) {
|
|
||||||
count.value = addressCount;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('address'),
|
|
||||||
key: "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('to_mail'),
|
|
||||||
key: "to_mail"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('subject'),
|
|
||||||
key: "subject"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('created_at'),
|
|
||||||
key: "created_at"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('action'),
|
|
||||||
key: 'actions',
|
|
||||||
render(row) {
|
|
||||||
return h('div', [
|
|
||||||
h(NButton,
|
|
||||||
{
|
|
||||||
type: 'success',
|
|
||||||
ghost: true,
|
|
||||||
onClick: () => {
|
|
||||||
showModal.value = true;
|
|
||||||
curRow.value = row;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => t('view') }
|
|
||||||
)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
watch([page, pageSize], async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="settings.address">
|
|
||||||
<n-modal v-model:show="showModal" preset="dialog" style="width: 100%;">
|
|
||||||
<pre style="overflow: auto;">{{ curRow.raw }}</pre>
|
|
||||||
</n-modal>
|
|
||||||
<div style="display: inline-block;">
|
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
|
||||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
|
||||||
<template #prefix="{ itemCount }">
|
|
||||||
{{ t('itemCount') }}: {{ itemCount }}
|
|
||||||
</template>
|
|
||||||
<template #suffix>
|
|
||||||
<n-button @click="fetchData" type="primary" size="small" tertiary>
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-pagination>
|
|
||||||
</div>
|
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.n-pagination {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -10,7 +10,7 @@ from twisted.internet import protocol, reactor, defer
|
|||||||
from twisted.cred.checkers import ICredentialsChecker, IUsernamePassword
|
from twisted.cred.checkers import ICredentialsChecker, IUsernamePassword
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
from parse_email import parse_email
|
from parse_email import generate_email_model, parse_email
|
||||||
from models import EmailModel
|
from models import EmailModel
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -58,7 +58,8 @@ class SimpleMessage:
|
|||||||
@implementer(imap4.IMailboxInfo, imap4.IMailbox)
|
@implementer(imap4.IMailboxInfo, imap4.IMailbox)
|
||||||
class SimpleMailbox:
|
class SimpleMailbox:
|
||||||
|
|
||||||
def __init__(self, password):
|
def __init__(self, name, password):
|
||||||
|
self.name = name
|
||||||
self.password = password
|
self.password = password
|
||||||
self.listeners = []
|
self.listeners = []
|
||||||
self.addListener = self.listeners.append
|
self.addListener = self.listeners.append
|
||||||
@@ -104,6 +105,13 @@ class SimpleMailbox:
|
|||||||
return defer.succeed(r)
|
return defer.succeed(r)
|
||||||
|
|
||||||
def fetch(self, messages, uid):
|
def fetch(self, messages, uid):
|
||||||
|
if self.name == "INBOX":
|
||||||
|
return self.fetchINBOX(messages)
|
||||||
|
if self.name == "SENT":
|
||||||
|
return self.fetchSENT(messages)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def fetchINBOX(self, messages):
|
||||||
start, end = messages.ranges[0]
|
start, end = messages.ranges[0]
|
||||||
start = max(start, 1)
|
start = max(start, 1)
|
||||||
if self.message_count > 0 and start > self.message_count:
|
if self.message_count > 0 and start > self.message_count:
|
||||||
@@ -127,6 +135,30 @@ class SimpleMailbox:
|
|||||||
for uid, item in enumerate(reversed(res.json()["results"]))
|
for uid, item in enumerate(reversed(res.json()["results"]))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def fetchSENT(self, messages):
|
||||||
|
start, end = messages.ranges[0]
|
||||||
|
start = max(start, 1)
|
||||||
|
if self.message_count > 0 and start > self.message_count:
|
||||||
|
return []
|
||||||
|
res = requests.get(
|
||||||
|
f"{settings.proxy_url}/api/sendbox?limit=20&offset={start - 1}", headers={
|
||||||
|
"Authorization": f"Bearer {self.password}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if res.status_code != 200:
|
||||||
|
_logger.error(
|
||||||
|
"Failed: "
|
||||||
|
f"code=[{res.status_code}] text=[{res.text}]"
|
||||||
|
)
|
||||||
|
raise Exception("Failed to fetch emails")
|
||||||
|
if res.json()["count"] > 0:
|
||||||
|
self.message_count = res.json()["count"]
|
||||||
|
return [
|
||||||
|
(start + uid, SimpleMessage(start + uid, generate_email_model(item)))
|
||||||
|
for uid, item in enumerate(reversed(res.json()["results"]))
|
||||||
|
]
|
||||||
|
|
||||||
def getUID(self, message):
|
def getUID(self, message):
|
||||||
return message.uid
|
return message.uid
|
||||||
|
|
||||||
@@ -141,11 +173,16 @@ class Account(imap4.MemoryAccount):
|
|||||||
self.password = password
|
self.password = password
|
||||||
super().__init__(user)
|
super().__init__(user)
|
||||||
|
|
||||||
|
def isSubscribed(self, name):
|
||||||
|
return name.upper() in ["INBOX", "SENT"]
|
||||||
|
|
||||||
def _emptyMailbox(self, name, id):
|
def _emptyMailbox(self, name, id):
|
||||||
_logger.info(f"New mailbox: {name}, {id}")
|
_logger.info(f"New mailbox: {name}, {id}")
|
||||||
if name != "INBOX":
|
if name == "INBOX":
|
||||||
raise imap4.NoSuchMailbox(name.encode("utf-8"))
|
return SimpleMailbox(name, self.password)
|
||||||
return SimpleMailbox(self.password)
|
if name == "SENT":
|
||||||
|
return SimpleMailbox(name, self.password)
|
||||||
|
raise imap4.NoSuchMailbox(name.encode("utf-8"))
|
||||||
|
|
||||||
def select(self, name, rw=1):
|
def select(self, name, rw=1):
|
||||||
return imap4.MemoryAccount.select(self, name)
|
return imap4.MemoryAccount.select(self, name)
|
||||||
@@ -166,6 +203,7 @@ class SimpleRealm:
|
|||||||
res = json.loads(avatarId)
|
res = json.loads(avatarId)
|
||||||
account = Account(res["username"], res["password"])
|
account = Account(res["username"], res["password"])
|
||||||
account.addMailbox("INBOX")
|
account.addMailbox("INBOX")
|
||||||
|
account.addMailbox("SENT")
|
||||||
return imap4.IAccount, account, lambda: None
|
return imap4.IAccount, account, lambda: None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import email
|
import datetime
|
||||||
from email.message import Message
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import email
|
||||||
|
|
||||||
|
from email.header import Header
|
||||||
|
from email.message import Message
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
|
||||||
from models import EmailModel
|
from models import EmailModel
|
||||||
|
|
||||||
@@ -36,3 +43,21 @@ def parse_email(raw: str) -> EmailModel:
|
|||||||
size=len("could not parse email"),
|
size=len("could not parse email"),
|
||||||
subparts=[],
|
subparts=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_email_model(item: dict) -> EmailModel:
|
||||||
|
email_json = json.loads(item["raw"])
|
||||||
|
message = MIMEMultipart()
|
||||||
|
message['From'] = f"{email_json["from"]['name']} <{
|
||||||
|
email_json["from"]['email']}>"
|
||||||
|
message['To'] = ", ".join(
|
||||||
|
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
|
||||||
|
message['Subject'] = email_json["subject"]
|
||||||
|
message["Date"] = datetime.datetime.strptime(
|
||||||
|
item["created_at"], "%Y-%m-%d %H:%M:%S"
|
||||||
|
).strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||||
|
message.attach(MIMEText(
|
||||||
|
email_json["content"][0]["value"],
|
||||||
|
"html" if "html" in email_json["content"][0]["type"] else "plain"
|
||||||
|
))
|
||||||
|
return parse_email(message.as_string())
|
||||||
|
|||||||
Reference in New Issue
Block a user