From 77155299e0ea550d0369f909da26ed15e6580ccc Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Sat, 1 Jun 2024 21:23:17 +0800 Subject: [PATCH] feat: add mailbox multi delete and download (#292) --- CHANGELOG.md | 1 + frontend/package.json | 1 + frontend/pnpm-lock.yaml | 80 +++++++ frontend/src/components/MailBox.vue | 340 ++++++++++++++++++++-------- 4 files changed, 326 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4b4ad5..133f1b6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - worker: 增加 `FORWARD_ADDRESS_LIST` 全局邮件转发地址(等同于 `catch all`) - UI: 多语言使用路由进行切换 - 添加保存附件到 S3 的功能 +- UI: 增加收取邮件列表 `批量删除` 和 `批量下载` ## v0.4.6 diff --git a/frontend/package.json b/frontend/package.json index abef1183..42492708 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 19178d0c..54c4b816 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -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)): diff --git a/frontend/src/components/MailBox.vue b/frontend/src/components/MailBox.vue index 712dbc03..ee692ad4 100644 --- a/frontend/src/components/MailBox.vue +++ b/frontend/src/components/MailBox.vue @@ -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(() => { - + {{ t('refresh') }} - +
@@ -433,6 +561,26 @@ onBeforeUnmount(() => { + + + {{ multiActionDownloadZip.filename }} + + + + {{ t('downloadMail') + " zip" }} + + + + + + + {{ multiActionDeleteProgress.tip }} + + + +