feat: add mailbox multi delete and download (#292)

This commit is contained in:
Dream Hunter
2024-06-01 21:23:17 +08:00
committed by GitHub
parent 9725407c77
commit 77155299e0
4 changed files with 326 additions and 96 deletions

View File

@@ -7,6 +7,7 @@
- worker: 增加 `FORWARD_ADDRESS_LIST` 全局邮件转发地址(等同于 `catch all`)
- UI: 多语言使用路由进行切换
- 添加保存附件到 S3 的功能
- UI: 增加收取邮件列表 `批量删除``批量下载`
## v0.4.6

View File

@@ -21,6 +21,7 @@
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.7.2",
"jszip": "^3.10.1",
"mail-parser-wasm": "^0.1.6",
"naive-ui": "^2.38.2",
"postal-mime": "^2.2.5",

View File

@@ -26,6 +26,9 @@ importers:
axios:
specifier: ^1.7.2
version: 1.7.2
jszip:
specifier: ^3.10.1
version: 3.10.1
mail-parser-wasm:
specifier: ^0.1.6
version: 0.1.6
@@ -1641,6 +1644,9 @@ packages:
core-js-compat@3.37.1:
resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
crypto-random-string@2.0.0:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
@@ -1969,6 +1975,9 @@ packages:
idb@7.1.1:
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
@@ -2081,6 +2090,9 @@ packages:
is-weakref@1.0.2:
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@@ -2123,10 +2135,16 @@ packages:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
local-pkg@0.5.0:
resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
engines: {node: '>=14'}
@@ -2267,6 +2285,9 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
@@ -2319,6 +2340,9 @@ packages:
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
engines: {node: '>=6'}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -2332,6 +2356,9 @@ packages:
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@@ -2410,6 +2437,9 @@ packages:
resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==}
engines: {node: '>=0.4'}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -2448,6 +2478,9 @@ packages:
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
engines: {node: '>= 0.4'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
side-channel@1.0.6:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
engines: {node: '>= 0.4'}
@@ -2508,6 +2541,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
stringify-object@3.3.0:
resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
engines: {node: '>=4'}
@@ -2680,6 +2716,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
@@ -4485,6 +4524,8 @@ snapshots:
dependencies:
browserslist: 4.23.0
core-util-is@1.0.3: {}
crypto-random-string@2.0.0: {}
css-render@0.15.14:
@@ -4891,6 +4932,8 @@ snapshots:
idb@7.1.1: {}
immediate@3.0.6: {}
immer@9.0.21: {}
inflight@1.0.6:
@@ -4991,6 +5034,8 @@ snapshots:
dependencies:
call-bind: 1.0.7
isarray@1.0.0: {}
isarray@2.0.5: {}
jake@10.9.1:
@@ -5026,8 +5071,19 @@ snapshots:
jsonpointer@5.0.1: {}
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
leven@3.1.0: {}
lie@3.3.0:
dependencies:
immediate: 3.0.6
local-pkg@0.5.0:
dependencies:
mlly: 1.7.0
@@ -5180,6 +5236,8 @@ snapshots:
dependencies:
wrappy: 1.0.2
pako@1.0.11: {}
path-is-absolute@1.0.1: {}
path-parse@1.0.7: {}
@@ -5218,6 +5276,8 @@ snapshots:
prismjs@1.29.0: {}
process-nextick-args@2.0.1: {}
proxy-from-env@1.1.0: {}
punycode@2.3.1: {}
@@ -5228,6 +5288,16 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
@@ -5335,6 +5405,8 @@ snapshots:
has-symbols: 1.0.3
isarray: 2.0.5
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
safe-regex-test@1.0.3:
@@ -5380,6 +5452,8 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
setimmediate@1.0.5: {}
side-channel@1.0.6:
dependencies:
call-bind: 1.0.7
@@ -5458,6 +5532,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.0.0
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
stringify-object@3.3.0:
dependencies:
get-own-enumerable-property-symbols: 3.0.2
@@ -5663,6 +5741,8 @@ snapshots:
dependencies:
punycode: 2.3.1
util-deprecate@1.0.2: {}
uuid@9.0.1: {}
vdirs@0.1.8(vue@3.4.27(typescript@5.4.5)):

View File

@@ -49,7 +49,7 @@ const props = defineProps({
})
const {
isDark, mailboxSplitSize, indexTab,
isDark, mailboxSplitSize, indexTab, loading,
useIframeShowMail, sendMailModel, preferShowTextMail
} = useGlobalState()
const autoRefresh = ref(false)
@@ -66,6 +66,12 @@ const curAttachments = ref([])
const curMail = ref(null);
const showTextMail = ref(preferShowTextMail.value)
const multiActionMode = ref(false)
const showMultiActionDownload = ref(false)
const showMultiActionDelete = ref(false)
const multiActionDownloadZip = ref({})
const multiActionDeleteProgress = ref({ percentage: 0, tip: '0/0' })
const { t } = useI18n({
messages: {
en: {
@@ -75,13 +81,17 @@ const { t } = useI18n({
refresh: 'Refresh',
attachments: 'Show Attachments',
downloadMail: 'Download Mail',
pleaseSelectMail: "Please select a mail to view.",
pleaseSelectMail: "Please select mail",
delete: 'Delete',
deleteMailTip: 'Are you sure you want to delete this mail?',
deleteMailTip: 'Are you sure you want to delete mail?',
reply: 'Reply',
showTextMail: 'Show Text Mail',
showHtmlMail: 'Show Html Mail',
saveToS3: 'Save to S3',
multiAction: 'Multi Action',
cancelMultiAction: 'Cancel Multi Action',
selectAll: 'Select All',
unselectAll: 'Unselect All',
},
zh: {
success: '成功',
@@ -90,13 +100,17 @@ const { t } = useI18n({
refresh: '刷新',
downloadMail: '下载邮件',
attachments: '查看附件',
pleaseSelectMail: "请选择一封邮件查看。",
pleaseSelectMail: "请选择邮件",
delete: '删除',
deleteMailTip: '确定要删除这封邮件吗?',
deleteMailTip: '确定要删除邮件吗?',
reply: '回复',
showTextMail: '显示纯文本邮件',
showHtmlMail: '显示HTML邮件',
saveToS3: '保存到S3',
multiAction: '多选',
cancelMultiAction: '取消多选',
selectAll: '全选',
unselectAll: '取消全选',
}
}
});
@@ -134,12 +148,14 @@ const refresh = async () => {
pageSize.value, (page.value - 1) * pageSize.value
);
data.value = await Promise.all(results.map(async (item) => {
item.checked = false;
return await processItem(item);
}));
if (totalCount > 0) {
count.value = totalCount;
}
if (!isMobile.value && !curMail.value && data.value.length > 0) {
curMail.value = null;
if (!isMobile.value && data.value.length > 0) {
curMail.value = data.value[0];
}
} catch (error) {
@@ -149,6 +165,10 @@ const refresh = async () => {
};
const clickRow = async (row) => {
if (multiActionMode.value) {
row.checked = !row.checked;
return;
}
curMail.value = row;
};
@@ -205,6 +225,82 @@ const saveToS3Proxy = async (filename, blob) => {
}
}
const multiActionModeClick = (enableMulti) => {
if (enableMulti) {
data.value.forEach((item) => {
item.checked = false;
});
multiActionMode.value = true;
} else {
multiActionMode.value = false;
data.value.forEach((item) => {
item.checked = false;
});
}
}
const multiActionSelectAll = (checked) => {
data.value.forEach((item) => {
item.checked = checked;
});
}
const multiActionDeleteMail = async () => {
try {
loading.value = true;
const selectedMails = data.value.filter((item) => item.checked);
if (selectedMails.length === 0) {
message.error(t('pleaseSelectMail'));
return;
}
multiActionDeleteProgress.value = {
percentage: 0,
tip: `0/${selectedMails.length}`
};
for (const [index, mail] of selectedMails.entries()) {
await props.deleteMail(mail.id);
showMultiActionDelete.value = true;
multiActionDeleteProgress.value = {
percentage: Math.floor((index + 1) / selectedMails.length * 100),
tip: `${index + 1}/${selectedMails.length}`
};
}
message.success(t("success"));
await refresh();
} catch (error) {
message.error(error.message || "error");
} finally {
loading.value = false;
showMultiActionDelete.value = true;
}
}
const multiActionDownload = async () => {
try {
loading.value = true;
const selectedMails = data.value.filter((item) => item.checked);
if (selectedMails.length === 0) {
message.error(t('pleaseSelectMail'));
return;
}
const JSZipModlue = await import('jszip');
const JSZip = JSZipModlue.default;
const zip = new JSZip();
for (const mail of selectedMails) {
zip.file(`${mail.id}.eml`, mail.raw);
}
multiActionDownloadZip.value = {
url: URL.createObjectURL(await zip.generateAsync({ type: "blob" })),
filename: `mails-${new Date().toISOString().replace(/:/g, '-')}.zip`
}
showMultiActionDownload.value = true;
} catch (error) {
message.error(error.message || "error");
} finally {
loading.value = false;
}
}
onMounted(async () => {
await refresh();
});
@@ -216,14 +312,38 @@ onBeforeUnmount(() => {
<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;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
</div>
<n-switch v-model:value="autoRefresh" size="small" :round="false">
<div v-if="!isMobile" class="left">
<div style="margin-bottom: 10px;">
<n-space v-if="multiActionMode">
<n-button @click="multiActionModeClick(false)" tertiary>
{{ t('cancelMultiAction') }}
</n-button>
<n-button @click="multiActionSelectAll(true)" tertiary>
{{ t('selectAll') }}
</n-button>
<n-button @click="multiActionSelectAll(false)" tertiary>
{{ t('unselectAll') }}
</n-button>
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="multiActionDeleteMail">
<template #trigger>
<n-button tertiary type="error">{{ t('delete') }}</n-button>
</template>
{{ t('deleteMailTip') }}
</n-popconfirm>
<n-button @click="multiActionDownload" tertiary type="info">
<template #icon>
<n-icon :component="CloudDownloadRound" />
</template>
{{ t('downloadMail') }}
</n-button>
</n-space>
<n-space v-else>
<n-button @click="multiActionModeClick(true)" type="primary" tertiary>
{{ t('multiAction') }}
</n-button>
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
show-size-picker />
<n-switch v-model:value="autoRefresh" :round="false">
<template #checked>
{{ t('refreshAfter', { msg: autoRefreshInterval }) }}
</template>
@@ -231,90 +351,98 @@ onBeforeUnmount(() => {
{{ t('autoRefresh') }}
</template>
</n-switch>
<n-button @click="refresh" size="small" type="primary" tertiary>
<n-button @click="refresh" 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} UTC` }}
</n-tag>
<n-tag type="info">
FROM: {{ row.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ row.address }}
</n-tag>
</n-space>
</div>
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
:on-update:size="onSpiltSizeChange">
<template #1>
<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)">
<template #prefix v-if="multiActionMode">
<n-checkbox v-model:checked="row.checked" />
</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} UTC` }}
</n-tag>
<n-tag type="info">
FROM: {{ curMail.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ curMail.address }}
</n-tag>
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
<template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template>
{{ t('deleteMailTip') }}
</n-popconfirm>
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
@click="getAttachments(curMail.attachments)">
{{ t('attachments') }}
</n-button>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
:href="getDownloadEmlUrl(curMail.raw)">
<template #icon>
<n-icon :component="CloudDownloadRound" />
</template>
{{ t('downloadMail') }}
</n-button>
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
<template #icon>
<n-icon :component="ReplyFilled" />
</template>
{{ t('reply') }}
</n-button>
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
</n-button>
</n-space>
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;">
</iframe>
<div v-else v-html="curMail.message" 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>
<n-thing :title="row.subject">
<template #description>
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
<n-tag type="info">
{{ `${row.created_at} UTC` }}
</n-tag>
<n-tag type="info">
FROM: {{ row.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ row.address }}
</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} UTC` }}
</n-tag>
<n-tag type="info">
FROM: {{ curMail.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ curMail.address }}
</n-tag>
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
<template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template>
{{ t('deleteMailTip') }}
</n-popconfirm>
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
@click="getAttachments(curMail.attachments)">
{{ t('attachments') }}
</n-button>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
:href="getDownloadEmlUrl(curMail.raw)">
<template #icon>
<n-icon :component="CloudDownloadRound" />
</template>
{{ t('downloadMail') }}
</n-button>
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
<template #icon>
<n-icon :component="ReplyFilled" />
</template>
{{ t('reply') }}
</n-button>
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
</n-button>
</n-space>
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;">
</iframe>
<div v-else v-html="curMail.message" 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>
<div class="left" v-else>
<div class="center">
<n-space justify="center">
<div style="display: inline-block;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
</div>
@@ -326,10 +454,10 @@ onBeforeUnmount(() => {
{{ t('autoRefresh') }}
</template>
</n-switch>
<n-button @click="refresh" size="small" type="primary">
<n-button @click="refresh" tertiary size="small" type="primary">
{{ t('refresh') }}
</n-button>
</div>
</n-space>
<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)">
@@ -433,6 +561,26 @@ onBeforeUnmount(() => {
</n-list>
</n-spin>
</n-modal>
<n-modal v-model:show="showMultiActionDownload" preset="dialog" :title="t('downloadMail')">
<n-tag type="info">
{{ multiActionDownloadZip.filename }}
</n-tag>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="multiActionDownloadZip.filename"
:href="multiActionDownloadZip.url">
<n-icon :component="CloudDownloadRound" />
{{ t('downloadMail') + " zip" }}
</n-button>
</n-modal>
<n-modal v-model:show="showMultiActionDelete" preset="dialog" :title="t('delete') + t('success')"
negative-text="OK">
<n-space justify="center">
<n-progress type="circle" status="error" :percentage="multiActionDeleteProgress.percentage">
<span style="text-align: center">
{{ multiActionDeleteProgress.tip }}
</span>
</n-progress>
</n-space>
</n-modal>
</div>
</template>