feat: add s3 attachment (#291)

This commit is contained in:
Dream Hunter
2024-06-01 20:08:42 +08:00
committed by GitHub
parent e91bbe273a
commit 9725407c77
18 changed files with 1584 additions and 42 deletions

View File

@@ -71,6 +71,7 @@ const getOpenSettings = async (message) => {
copyright: res["copyright"] || openSettings.value.copyright,
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
enableWebhook: res["enableWebhook"] || false,
isS3Enabled: res["isS3Enabled"] || false,
});
if (openSettings.value.needAuth) {
showAuth.value = true;

View File

@@ -35,7 +35,17 @@ const props = defineProps({
type: Boolean,
default: false,
requried: false
}
},
showSaveS3: {
type: Boolean,
default: false,
requried: false
},
saveToS3: {
type: Function,
default: (mail_id, filename, blob) => { },
requried: false
},
})
const {
@@ -70,7 +80,8 @@ const { t } = useI18n({
deleteMailTip: 'Are you sure you want to delete this mail?',
reply: 'Reply',
showTextMail: 'Show Text Mail',
showHtmlMail: 'Show Html Mail'
showHtmlMail: 'Show Html Mail',
saveToS3: 'Save to S3',
},
zh: {
success: '成功',
@@ -84,7 +95,8 @@ const { t } = useI18n({
deleteMailTip: '确定要删除这封邮件吗?',
reply: '回复',
showTextMail: '显示纯文本邮件',
showHtmlMail: '显示HTML邮件'
showHtmlMail: '显示HTML邮件',
saveToS3: '保存到S3',
}
}
});
@@ -183,6 +195,16 @@ const onSpiltSizeChange = (size) => {
mailboxSplitSize.value = size;
}
const attachmentLoding = ref(false)
const saveToS3Proxy = async (filename, blob) => {
attachmentLoding.value = true
try {
await props.saveToS3(curMail.value.id, filename, blob);
} finally {
attachmentLoding.value = false
}
}
onMounted(async () => {
await refresh();
});
@@ -385,25 +407,31 @@ onBeforeUnmount(() => {
<template #header>
<div>{{ t("attachments") }}</div>
</template>
<n-list hoverable clickable>
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
<n-thing class="center" :title="row.filename">
<template #description>
<n-space>
<n-tag type="info">
Size: {{ row.size }}
</n-tag>
</n-space>
<n-spin v-model:show="attachmentLoding">
<n-list hoverable clickable>
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
<n-thing class="center" :title="row.filename">
<template #description>
<n-space>
<n-tag type="info">
Size: {{ row.size }}
</n-tag>
<n-button v-if="showSaveS3" @click="saveToS3Proxy(row.filename, row.blob)" ghost type="info"
size="small">
{{ t('saveToS3') }}
</n-button>
</n-space>
</template>
</n-thing>
<template #suffix>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
:href="row.url">
<n-icon :component="CloudDownloadRound" />
</n-button>
</template>
</n-thing>
<template #suffix>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
:href="row.url">
<n-icon :component="CloudDownloadRound" />
</n-button>
</template>
</n-list-item>
</n-list>
</n-list-item>
</n-list>
</n-spin>
</n-modal>
</div>
</template>

View File

@@ -18,6 +18,8 @@ export const useGlobalState = createGlobalState(
domains: [],
copyright: 'Dream Hunter',
cfTurnstileSiteKey: '',
enableWebhook: false,
isS3Enabled: false,
})
const settings = ref({
fetched: false,

View File

@@ -16,11 +16,11 @@ export async function processItem(item) {
item.message = parsedEmail.body_html || parsedEmail.text || '';
item.text = parsedEmail.text || '';
item.attachments = parsedEmail.attachments?.map((a_item) => {
const blob_url = URL.createObjectURL(
new Blob(
[a_item.content],
{ type: a_item.content_type || 'application/octet-stream' }
))
const blob = new Blob(
[a_item.content],
{ type: a_item.content_type || 'application/octet-stream' }
);
const blob_url = URL.createObjectURL(blob);
if (a_item.content_id && a_item.content_id.length > 0) {
item.message = item.message.replace(`cid:${a_item.content_id}`, blob_url);
}
@@ -28,7 +28,8 @@ export async function processItem(item) {
id: a_item.content_id || Math.random().toString(36).substring(2, 15),
filename: a_item.filename || a_item.content_id || "",
size: humanFileSize(a_item.content?.length || 0),
url: blob_url
url: blob_url,
blob: blob
}
}) || [];
} catch (error) {
@@ -49,11 +50,11 @@ export async function processItem(item) {
item.message = parsedEmail.html || parsedEmail.text || item.raw;
item.text = parsedEmail.text || '';
item.attachments = parsedEmail.attachments?.map((a_item) => {
const blob_url = URL.createObjectURL(
new Blob(
[a_item.content],
{ type: a_item.mimeType || 'application/octet-stream' }
))
const blob = new Blob(
[a_item.content],
{ type: a_item.mimeType || 'application/octet-stream' }
);
const blob_url = URL.createObjectURL(blob)
if (a_item.contentId && a_item.contentId.length > 0) {
item.message = item.message.replace(`cid:${a_item.contentId}`, blob_url);
}
@@ -61,7 +62,8 @@ export async function processItem(item) {
id: a_item.contentId || Math.random().toString(36).substring(2, 15),
filename: a_item.filename || a_item.contentId || "",
size: humanFileSize(a_item.content?.length || 0),
url: blob_url
url: blob_url,
blob: blob
}
}) || [];
} catch (error) {

View File

@@ -10,11 +10,13 @@ import MailBox from '../components/MailBox.vue';
import SendBox from '../components/SendBox.vue';
import AutoReply from './index/AutoReply.vue';
import AccountSettings from './index/AccountSettings.vue';
import WenHook from './index/Webhook.vue';
import Webhook from './index/Webhook.vue';
import Attachment from './index/Attachment.vue';
import About from './common/About.vue';
const SendMail = defineAsyncComponent(() => import('./index/SendMail.vue'));
const { settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
const message = useMessage()
const { t } = useI18n({
messages: {
@@ -25,6 +27,8 @@ const { t } = useI18n({
auto_reply: 'Auto Reply',
accountSettings: 'Account Settings',
about: 'About',
s3Attachment: 'S3 Attachment',
saveToS3Success: 'save to s3 success',
},
zh: {
mailbox: '收件箱',
@@ -33,6 +37,8 @@ const { t } = useI18n({
auto_reply: '自动回复',
accountSettings: '账户设置',
about: '关于',
s3Attachment: 'S3附件',
saveToS3Success: '保存到s3成功',
}
}
});
@@ -48,6 +54,26 @@ const deleteMail = async (curMailId) => {
const fetchSenboxData = async (limit, offset) => {
return await api.fetch(`/api/sendbox?limit=${limit}&offset=${offset}`);
};
const saveToS3 = async (mail_id, filename, blob) => {
try {
const { url } = await api.fetch(`/api/attachment/put_url`, {
method: 'POST',
body: JSON.stringify({ key: `${mail_id}/${filename}` })
});
// upload to s3 by formdata
const formData = new FormData();
formData.append(filename, blob);
await fetch(url, {
method: 'PUT',
body: formData
});
message.success(t('saveToS3Success'));
} catch (error) {
console.error(error);
message.error(error.message || "save to s3 error");
}
}
</script>
<template>
@@ -55,8 +81,9 @@ const fetchSenboxData = async (limit, offset) => {
<AddressBar />
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
<n-tab-pane name="mailbox" :tab="t('mailbox')">
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
<MailBox :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled" :saveToS3="saveToS3"
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :fetchMailData="fetchMailData"
:deleteMail="deleteMail" />
</n-tab-pane>
<n-tab-pane name="sendbox" :tab="t('sendbox')">
<SendBox :fetchMailData="fetchSenboxData" />
@@ -71,7 +98,10 @@ const fetchSenboxData = async (limit, offset) => {
<AutoReply />
</n-tab-pane>
<n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhook')">
<WenHook />
<Webhook />
</n-tab-pane>
<n-tab-pane v-if="openSettings.isS3Enabled" name="s3_attachment" :tab="t('s3Attachment')">
<Attachment />
</n-tab-pane>
<n-tab-pane v-if="openSettings.enableIndexAbout" name="about" :tab="t('about')">
<About />

View File

@@ -0,0 +1,91 @@
<script setup>
import { ref, h, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'
import { api } from '../../api'
const message = useMessage()
const { t } = useI18n({
messages: {
en: {
download: 'Download',
action: 'Action',
},
zh: {
download: '下载',
action: '操作',
}
}
});
const data = ref([])
const showDownload = ref(false)
const curRow = ref({})
const curDownloadUrl = ref('')
const fetchData = async () => {
try {
const { results } = await api.fetch(
`/api/attachment/list`
);
data.value = results;
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const columns = [
{
title: "key",
key: "key"
},
{
title: t('action'),
key: 'actions',
render(row) {
return h('div', [
h(NButton,
{
type: 'success',
tertiary: true,
onClick: async () => {
try {
const { url } = await api.fetch(`/api/attachment/get_url`, {
method: 'POST',
body: JSON.stringify({ key: row.key })
});
curDownloadUrl.value = url;
curRow.value = row;
showDownload.value = true;
}
catch (error) {
console.error(error);
message.error(error.message || "error");
}
}
},
{ default: () => t('download') }
)
])
}
}
]
onMounted(async () => {
await fetchData()
})
</script>
<template>
<div>
<n-modal v-model:show="showDownload" preset="dialog" :title="t('download')">
<n-tag type="info">{{ curRow.key }}</n-tag>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curRow.key.replace('/', '_')"
:href="curDownloadUrl">
{{ t('download') }}
</n-button>
</n-modal>
<n-data-table :columns="columns" :data="data" :bordered="false" />
</div>
</template>