feat: optimize email filtering with frontend-only search (#787)

* feat: optimize email filtering with frontend-only search

- Remove backend keyword parameter from mail APIs (breaking change)
- Implement frontend filtering on current page (20-100 items)
- Add message_id database index for UPDATE performance
- Support desktop and mobile responsive layouts
- Update API documentation and CHANGELOG

BREAKING CHANGE: /admin/mails and /user_api/mails no longer accept keyword parameter

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: restore Mail ID query input in Index.vue

- Keep showMailIdQuery UI input for querying specific mail by ID
- Triggered when URL contains mail_id parameter

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2025-12-15 02:55:50 +08:00
committed by GitHub
co-authored by Claude Sonnet 4.5
parent 1836f931ee
commit e5f62d4713
17 changed files with 489 additions and 546 deletions
+2
View File
@@ -3,6 +3,8 @@
## v1.2.0(main) ## v1.2.0(main)
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
- feat: |数据库| 为 `message_id` 字段添加索引,优化邮件更新操作性能,需执行 `db/2025-12-15-message-id-index.sql` 更新数据库
- feat: |Admin| 维护页面增加自定义 SQL 清理功能,支持定时任务执行自定义清理语句 - feat: |Admin| 维护页面增加自定义 SQL 清理功能,支持定时任务执行自定义清理语句
## v1.1.0 ## v1.1.0
+4
View File
@@ -0,0 +1,4 @@
-- Add index on message_id column in raw_mails table
-- This index improves performance for queries filtering/updating by message_id
-- Example: UPDATE raw_mails SET metadata = ? WHERE message_id = ?
CREATE INDEX IF NOT EXISTS idx_raw_mails_message_id ON raw_mails(message_id);
+2
View File
@@ -12,6 +12,8 @@ CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at); CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at);
CREATE INDEX IF NOT EXISTS idx_raw_mails_message_id ON raw_mails(message_id);
CREATE TABLE IF NOT EXISTS address ( CREATE TABLE IF NOT EXISTS address (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE, name TEXT UNIQUE,
+1 -1
View File
@@ -40,7 +40,7 @@
"devDependencies": { "devDependencies": {
"@vicons/fa": "^0.13.0", "@vicons/fa": "^0.13.0",
"@vicons/material": "^0.13.0", "@vicons/material": "^0.13.0",
"@vitejs/plugin-vue": "^6.0.2", "@vitejs/plugin-vue": "^6.0.3",
"unplugin-auto-import": "^20.3.0", "unplugin-auto-import": "^20.3.0",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^30.0.0",
"vite": "^7.2.7", "vite": "^7.2.7",
+95 -87
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.1 version: 5.0.1
'@simplewebauthn/browser': '@simplewebauthn/browser':
specifier: 13.2.2 specifier: 10.0.0
version: 13.2.2 version: 10.0.0
'@unhead/vue': '@unhead/vue':
specifier: ^2.0.19 specifier: ^2.0.19
version: 2.0.19(vue@3.5.25(typescript@5.4.5)) version: 2.0.19(vue@3.5.25(typescript@5.4.5))
@@ -64,8 +64,8 @@ importers:
specifier: ^0.13.0 specifier: ^0.13.0
version: 0.13.0 version: 0.13.0
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^6.0.2 specifier: ^6.0.3
version: 6.0.2(vite@7.2.7(terser@5.44.1))(vue@3.5.25(typescript@5.4.5)) version: 6.0.3(vite@7.2.7(terser@5.44.1))(vue@3.5.25(typescript@5.4.5))
unplugin-auto-import: unplugin-auto-import:
specifier: ^20.3.0 specifier: ^20.3.0
version: 20.3.0(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.4.5))) version: 20.3.0(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.4.5)))
@@ -1129,17 +1129,17 @@ packages:
'@juggle/resize-observer@3.4.0': '@juggle/resize-observer@3.4.0':
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
'@poppinss/colors@4.1.5': '@poppinss/colors@4.1.6':
resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==} resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
'@poppinss/dumper@0.6.5': '@poppinss/dumper@0.6.5':
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
'@poppinss/exception@1.2.2': '@poppinss/exception@1.2.3':
resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
'@rolldown/pluginutils@1.0.0-beta.50': '@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==} resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
'@rollup/plugin-babel@5.3.1': '@rollup/plugin-babel@5.3.1':
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
@@ -1309,8 +1309,12 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@simplewebauthn/browser@13.2.2': '@simplewebauthn/browser@10.0.0':
resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==} resolution: {integrity: sha512-hG0JMZD+LiLUbpQcAjS4d+t4gbprE/dLYop/CkE01ugU/9sKXflxV5s0DRjdz3uNMFecatRfb4ZLG3XvF8m5zg==}
'@simplewebauthn/types@10.0.0':
resolution: {integrity: sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
'@sindresorhus/is@7.1.1': '@sindresorhus/is@7.1.1':
resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==} resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==}
@@ -1322,68 +1326,68 @@ packages:
'@surma/rollup-plugin-off-main-thread@2.2.3': '@surma/rollup-plugin-off-main-thread@2.2.3':
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
'@swc/core-darwin-arm64@1.15.3': '@swc/core-darwin-arm64@1.15.4':
resolution: {integrity: sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==} resolution: {integrity: sha512-NU/Of+ShFGG/i0lXKsF6GaGeTBNsr9iD8uUzdXxFfGbEjTeuKNXc5CWn3/Uo4Gr4LMAGD3hsRwG2Jq5iBDMalw==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@swc/core-darwin-x64@1.15.3': '@swc/core-darwin-x64@1.15.4':
resolution: {integrity: sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==} resolution: {integrity: sha512-9oWYMZHiEfHLqjjRGrXL17I8HdAOpWK/Rps34RKQ74O+eliygi1Iyq1TDUzYqUXcNvqN2K5fHgoMLRIni41ClQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@swc/core-linux-arm-gnueabihf@1.15.3': '@swc/core-linux-arm-gnueabihf@1.15.4':
resolution: {integrity: sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==} resolution: {integrity: sha512-I1dPxXli3N1Vr71JXogUTLcspM5ICgCYaA16RE+JKchj3XKKmxLlYjwAHAA4lh/Cy486ikzACaG6pIBcegoGkg==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@swc/core-linux-arm64-gnu@1.15.3': '@swc/core-linux-arm64-gnu@1.15.4':
resolution: {integrity: sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==} resolution: {integrity: sha512-iGpuS/2PDZ68ioAlhkxiN5M4+pB9uDJolTKk4mZ0JM29uFf9YIkiyk7Bbr2y1QtmD82rF0tDHhoG9jtnV8mZMg==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@swc/core-linux-arm64-musl@1.15.3': '@swc/core-linux-arm64-musl@1.15.4':
resolution: {integrity: sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==} resolution: {integrity: sha512-Ly95wc+VXDhl08pjAoPUhVu5vNbuPMbURknRZa5QOZuiizJ6DkaSI0/zsEc26PpC6HTc4prNLY3ARVwZ7j/IJQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@swc/core-linux-x64-gnu@1.15.3': '@swc/core-linux-x64-gnu@1.15.4':
resolution: {integrity: sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==} resolution: {integrity: sha512-7pIG0BnaMn4zTpHeColPwyrWoTY9Drr+ISZQIgYHUKh3oaPtNCrXb289ScGbPPPjLsSfcGTeOy2pXmNczMC+yg==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@swc/core-linux-x64-musl@1.15.3': '@swc/core-linux-x64-musl@1.15.4':
resolution: {integrity: sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==} resolution: {integrity: sha512-oaqTV25V9H+PpSkvTcK25q6Q56FvXc6d2xBu486dv9LAPCHWgeAworE8WpBLV26g8rubcN5nGhO5HwSunXA7Ww==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@swc/core-win32-arm64-msvc@1.15.3': '@swc/core-win32-arm64-msvc@1.15.4':
resolution: {integrity: sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==} resolution: {integrity: sha512-VcPuUJw27YbGo1HcOaAriI50dpM3ZZeDW3x2cMnJW6vtkeyzUFk1TADmTwFax0Fn+yicCxhaWjnFE3eAzGAxIQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@swc/core-win32-ia32-msvc@1.15.3': '@swc/core-win32-ia32-msvc@1.15.4':
resolution: {integrity: sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==} resolution: {integrity: sha512-dREjghAZEuKAK9nQzJETAiCSihSpAVS6Vk9+y2ElaoeTj68tNB1txV/m1RTPPD/+Kgbz6ITPNyXRWxPdkP5aXw==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@swc/core-win32-x64-msvc@1.15.3': '@swc/core-win32-x64-msvc@1.15.4':
resolution: {integrity: sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==} resolution: {integrity: sha512-o/odIBuQkoxKbRweJWOMI9LeRSOenFKN2zgPeaaNQ/cyuVk2r6DCAobKMOodvDdZWlMn6N1xJrldeCRSTZIgiQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@swc/core@1.15.3': '@swc/core@1.15.4':
resolution: {integrity: sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==} resolution: {integrity: sha512-fH81BPo6EiJ7BUb6Qa5SY/NLWIRVambqU3740g0XPFPEz5KFPnzRYpR6zodQNOcEb9XUtZzRO1Y0WyIJP7iBxQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
'@swc/helpers': '>=0.5.17' '@swc/helpers': '>=0.5.17'
@@ -1397,8 +1401,8 @@ packages:
'@swc/types@0.1.25': '@swc/types@0.1.25':
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
'@swc/wasm@1.15.3': '@swc/wasm@1.15.4':
resolution: {integrity: sha512-NrjGmAplk+v4wokIaLxp1oLoCMVqdQcWoBXopQg57QqyPRcJXLKe+kg5ehhW6z8XaU4Bu5cRkDxUTDY5P0Zy9Q==} resolution: {integrity: sha512-waPWVrijKwM8qpUEJGpnQRDlpNE4nQyRmPxjjbJ+5dpmua132o1Nd9f5KQQIgVQqAKSKshKMHskIec8ussNsUg==}
'@transloadit/prettier-bytes@0.0.7': '@transloadit/prettier-bytes@0.0.7':
resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==} resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==}
@@ -1458,11 +1462,11 @@ packages:
'@vicons/material@0.13.0': '@vicons/material@0.13.0':
resolution: {integrity: sha512-lKVxFNprM+CaBkUH3gt6VjIeiMsKQl2zARQMwTCZruQl2vRHzyeZiKeCflWS99CEfv2JzX/6y697smxlzyxcVw==} resolution: {integrity: sha512-lKVxFNprM+CaBkUH3gt6VjIeiMsKQl2zARQMwTCZruQl2vRHzyeZiKeCflWS99CEfv2JzX/6y697smxlzyxcVw==}
'@vitejs/plugin-vue@6.0.2': '@vitejs/plugin-vue@6.0.3':
resolution: {integrity: sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==} resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies: peerDependencies:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
vue: ^3.2.25 vue: ^3.2.25
'@vue/compiler-core@3.5.25': '@vue/compiler-core@3.5.25':
@@ -1680,8 +1684,8 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
baseline-browser-mapping@2.9.6: baseline-browser-mapping@2.9.7:
resolution: {integrity: sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==} resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
hasBin: true hasBin: true
blake3-wasm@2.1.5: blake3-wasm@2.1.5:
@@ -1871,8 +1875,8 @@ packages:
error-stack-parser-es@1.0.5: error-stack-parser-es@1.0.5:
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
es-abstract@1.24.0: es-abstract@1.24.1:
resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-define-property@1.0.1: es-define-property@1.0.1:
@@ -2844,8 +2848,8 @@ packages:
resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
unimport@5.5.0: unimport@5.6.0:
resolution: {integrity: sha512-/JpWMG9s1nBSlXJAQ8EREFTFy3oy6USFd8T6AoBaw1q2GGcF4R9yp3ofg32UODZlYEO5VD0EWE1RpI9XDWyPYg==} resolution: {integrity: sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A==}
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
unique-string@2.0.0: unique-string@2.0.0:
@@ -4132,19 +4136,19 @@ snapshots:
'@juggle/resize-observer@3.4.0': {} '@juggle/resize-observer@3.4.0': {}
'@poppinss/colors@4.1.5': '@poppinss/colors@4.1.6':
dependencies: dependencies:
kleur: 4.1.5 kleur: 4.1.5
'@poppinss/dumper@0.6.5': '@poppinss/dumper@0.6.5':
dependencies: dependencies:
'@poppinss/colors': 4.1.5 '@poppinss/colors': 4.1.6
'@sindresorhus/is': 7.1.1 '@sindresorhus/is': 7.1.1
supports-color: 10.2.2 supports-color: 10.2.2
'@poppinss/exception@1.2.2': {} '@poppinss/exception@1.2.3': {}
'@rolldown/pluginutils@1.0.0-beta.50': {} '@rolldown/pluginutils@1.0.0-beta.53': {}
'@rollup/plugin-babel@5.3.1(@babel/core@7.28.5)(rollup@2.79.2)': '@rollup/plugin-babel@5.3.1(@babel/core@7.28.5)(rollup@2.79.2)':
dependencies: dependencies:
@@ -4264,7 +4268,11 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.53.3': '@rollup/rollup-win32-x64-msvc@4.53.3':
optional: true optional: true
'@simplewebauthn/browser@13.2.2': {} '@simplewebauthn/browser@10.0.0':
dependencies:
'@simplewebauthn/types': 10.0.0
'@simplewebauthn/types@10.0.0': {}
'@sindresorhus/is@7.1.1': {} '@sindresorhus/is@7.1.1': {}
@@ -4277,51 +4285,51 @@ snapshots:
magic-string: 0.25.9 magic-string: 0.25.9
string.prototype.matchall: 4.0.12 string.prototype.matchall: 4.0.12
'@swc/core-darwin-arm64@1.15.3': '@swc/core-darwin-arm64@1.15.4':
optional: true optional: true
'@swc/core-darwin-x64@1.15.3': '@swc/core-darwin-x64@1.15.4':
optional: true optional: true
'@swc/core-linux-arm-gnueabihf@1.15.3': '@swc/core-linux-arm-gnueabihf@1.15.4':
optional: true optional: true
'@swc/core-linux-arm64-gnu@1.15.3': '@swc/core-linux-arm64-gnu@1.15.4':
optional: true optional: true
'@swc/core-linux-arm64-musl@1.15.3': '@swc/core-linux-arm64-musl@1.15.4':
optional: true optional: true
'@swc/core-linux-x64-gnu@1.15.3': '@swc/core-linux-x64-gnu@1.15.4':
optional: true optional: true
'@swc/core-linux-x64-musl@1.15.3': '@swc/core-linux-x64-musl@1.15.4':
optional: true optional: true
'@swc/core-win32-arm64-msvc@1.15.3': '@swc/core-win32-arm64-msvc@1.15.4':
optional: true optional: true
'@swc/core-win32-ia32-msvc@1.15.3': '@swc/core-win32-ia32-msvc@1.15.4':
optional: true optional: true
'@swc/core-win32-x64-msvc@1.15.3': '@swc/core-win32-x64-msvc@1.15.4':
optional: true optional: true
'@swc/core@1.15.3': '@swc/core@1.15.4':
dependencies: dependencies:
'@swc/counter': 0.1.3 '@swc/counter': 0.1.3
'@swc/types': 0.1.25 '@swc/types': 0.1.25
optionalDependencies: optionalDependencies:
'@swc/core-darwin-arm64': 1.15.3 '@swc/core-darwin-arm64': 1.15.4
'@swc/core-darwin-x64': 1.15.3 '@swc/core-darwin-x64': 1.15.4
'@swc/core-linux-arm-gnueabihf': 1.15.3 '@swc/core-linux-arm-gnueabihf': 1.15.4
'@swc/core-linux-arm64-gnu': 1.15.3 '@swc/core-linux-arm64-gnu': 1.15.4
'@swc/core-linux-arm64-musl': 1.15.3 '@swc/core-linux-arm64-musl': 1.15.4
'@swc/core-linux-x64-gnu': 1.15.3 '@swc/core-linux-x64-gnu': 1.15.4
'@swc/core-linux-x64-musl': 1.15.3 '@swc/core-linux-x64-musl': 1.15.4
'@swc/core-win32-arm64-msvc': 1.15.3 '@swc/core-win32-arm64-msvc': 1.15.4
'@swc/core-win32-ia32-msvc': 1.15.3 '@swc/core-win32-ia32-msvc': 1.15.4
'@swc/core-win32-x64-msvc': 1.15.3 '@swc/core-win32-x64-msvc': 1.15.4
'@swc/counter@0.1.3': {} '@swc/counter@0.1.3': {}
@@ -4329,7 +4337,7 @@ snapshots:
dependencies: dependencies:
'@swc/counter': 0.1.3 '@swc/counter': 0.1.3
'@swc/wasm@1.15.3': {} '@swc/wasm@1.15.4': {}
'@transloadit/prettier-bytes@0.0.7': {} '@transloadit/prettier-bytes@0.0.7': {}
@@ -4392,9 +4400,9 @@ snapshots:
'@vicons/material@0.13.0': {} '@vicons/material@0.13.0': {}
'@vitejs/plugin-vue@6.0.2(vite@7.2.7(terser@5.44.1))(vue@3.5.25(typescript@5.4.5))': '@vitejs/plugin-vue@6.0.3(vite@7.2.7(terser@5.44.1))(vue@3.5.25(typescript@5.4.5))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.50 '@rolldown/pluginutils': 1.0.0-beta.53
vite: 7.2.7(terser@5.44.1) vite: 7.2.7(terser@5.44.1)
vue: 3.5.25(typescript@5.4.5) vue: 3.5.25(typescript@5.4.5)
@@ -4608,7 +4616,7 @@ snapshots:
array-buffer-byte-length: 1.0.2 array-buffer-byte-length: 1.0.2
call-bind: 1.0.8 call-bind: 1.0.8
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.24.0 es-abstract: 1.24.1
es-errors: 1.3.0 es-errors: 1.3.0
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
is-array-buffer: 3.0.5 is-array-buffer: 3.0.5
@@ -4661,7 +4669,7 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
baseline-browser-mapping@2.9.6: {} baseline-browser-mapping@2.9.7: {}
blake3-wasm@2.1.5: {} blake3-wasm@2.1.5: {}
@@ -4671,7 +4679,7 @@ snapshots:
browserslist@4.28.1: browserslist@4.28.1:
dependencies: dependencies:
baseline-browser-mapping: 2.9.6 baseline-browser-mapping: 2.9.7
caniuse-lite: 1.0.30001760 caniuse-lite: 1.0.30001760
electron-to-chromium: 1.5.267 electron-to-chromium: 1.5.267
node-releases: 2.0.27 node-releases: 2.0.27
@@ -4844,7 +4852,7 @@ snapshots:
error-stack-parser-es@1.0.5: {} error-stack-parser-es@1.0.5: {}
es-abstract@1.24.0: es-abstract@1.24.1:
dependencies: dependencies:
array-buffer-byte-length: 1.0.2 array-buffer-byte-length: 1.0.2
arraybuffer.prototype.slice: 1.0.4 arraybuffer.prototype.slice: 1.0.4
@@ -5581,7 +5589,7 @@ snapshots:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.24.0 es-abstract: 1.24.1
es-errors: 1.3.0 es-errors: 1.3.0
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
@@ -5845,7 +5853,7 @@ snapshots:
call-bind: 1.0.8 call-bind: 1.0.8
call-bound: 1.0.4 call-bound: 1.0.4
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.24.0 es-abstract: 1.24.1
es-errors: 1.3.0 es-errors: 1.3.0
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
@@ -5862,7 +5870,7 @@ snapshots:
call-bound: 1.0.4 call-bound: 1.0.4
define-data-property: 1.1.4 define-data-property: 1.1.4
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.24.0 es-abstract: 1.24.1
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
@@ -6011,7 +6019,7 @@ snapshots:
unicode-property-aliases-ecmascript@2.2.0: {} unicode-property-aliases-ecmascript@2.2.0: {}
unimport@5.5.0: unimport@5.6.0:
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0
escape-string-regexp: 5.0.0 escape-string-regexp: 5.0.0
@@ -6039,7 +6047,7 @@ snapshots:
local-pkg: 1.1.2 local-pkg: 1.1.2
magic-string: 0.30.21 magic-string: 0.30.21
picomatch: 4.0.3 picomatch: 4.0.3
unimport: 5.5.0 unimport: 5.6.0
unplugin: 2.3.11 unplugin: 2.3.11
unplugin-utils: 0.3.1 unplugin-utils: 0.3.1
optionalDependencies: optionalDependencies:
@@ -6104,8 +6112,8 @@ snapshots:
vite-plugin-top-level-await@1.6.0(rollup@2.79.2)(vite@7.2.7(terser@5.44.1)): vite-plugin-top-level-await@1.6.0(rollup@2.79.2)(vite@7.2.7(terser@5.44.1)):
dependencies: dependencies:
'@rollup/plugin-virtual': 3.0.2(rollup@2.79.2) '@rollup/plugin-virtual': 3.0.2(rollup@2.79.2)
'@swc/core': 1.15.3 '@swc/core': 1.15.4
'@swc/wasm': 1.15.3 '@swc/wasm': 1.15.4
uuid: 10.0.0 uuid: 10.0.0
vite: 7.2.7(terser@5.44.1) vite: 7.2.7(terser@5.44.1)
transitivePeerDependencies: transitivePeerDependencies:
@@ -6382,12 +6390,12 @@ snapshots:
youch-core@0.3.3: youch-core@0.3.3:
dependencies: dependencies:
'@poppinss/exception': 1.2.2 '@poppinss/exception': 1.2.3
error-stack-parser-es: 1.0.5 error-stack-parser-es: 1.0.5
youch@4.1.0-beta.10: youch@4.1.0-beta.10:
dependencies: dependencies:
'@poppinss/colors': 4.1.5 '@poppinss/colors': 4.1.6
'@poppinss/dumper': 0.6.5 '@poppinss/dumper': 0.6.5
'@speed-highlight/core': 1.2.12 '@speed-highlight/core': 1.2.12
cookie: 1.1.1 cookie: 1.1.1
+41 -8
View File
@@ -49,20 +49,44 @@ const props = defineProps({
default: (mail_id, filename, blob) => { }, default: (mail_id, filename, blob) => { },
required: false required: false
}, },
showFilterInput: {
type: Boolean,
default: false,
required: false
},
}) })
const localFilterKeyword = ref('')
const { const {
isDark, mailboxSplitSize, indexTab, loading, useUTCDate, isDark, mailboxSplitSize, indexTab, loading, useUTCDate,
autoRefresh, configAutoRefreshInterval, sendMailModel autoRefresh, configAutoRefreshInterval, sendMailModel
} = useGlobalState() } = useGlobalState()
const autoRefreshInterval = ref(configAutoRefreshInterval.value) const autoRefreshInterval = ref(configAutoRefreshInterval.value)
const data = ref([]) const rawData = ref([])
const timer = ref(null) const timer = ref(null)
const count = ref(0) const count = ref(0)
const page = ref(1) const page = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
// Computed property for filtered data (only filter current page)
const data = computed(() => {
if (!localFilterKeyword.value || localFilterKeyword.value.trim() === '') {
return rawData.value;
}
const keyword = localFilterKeyword.value.toLowerCase();
return rawData.value.filter(mail => {
// Search in subject, text, message fields
const searchFields = [
mail.subject || '',
mail.text || '',
mail.message || ''
].map(field => field.toLowerCase());
return searchFields.some(field => field.includes(keyword));
});
})
const canGoPrevMail = computed(() => { const canGoPrevMail = computed(() => {
if (!curMail.value) return false if (!curMail.value) return false
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id) const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
@@ -136,6 +160,8 @@ const { t } = useI18n({
unselectAll: 'Unselect All', unselectAll: 'Unselect All',
prevMail: 'Previous', prevMail: 'Previous',
nextMail: 'Next', nextMail: 'Next',
keywordQueryTip: 'Filter current page',
query: 'Query',
}, },
zh: { zh: {
success: '成功', success: '成功',
@@ -158,6 +184,8 @@ const { t } = useI18n({
unselectAll: '取消全选', unselectAll: '取消全选',
prevMail: '上一封', prevMail: '上一封',
nextMail: '下一封', nextMail: '下一封',
keywordQueryTip: '过滤当前页',
query: '查询',
} }
} }
}); });
@@ -197,7 +225,7 @@ const refresh = async () => {
pageSize.value, (page.value - 1) * pageSize.value pageSize.value, (page.value - 1) * pageSize.value
); );
loading.value = true; loading.value = true;
data.value = await Promise.all(results.map(async (item) => { rawData.value = await Promise.all(results.map(async (item) => {
item.checked = false; item.checked = false;
return await processItem(item); return await processItem(item);
})); }));
@@ -370,7 +398,7 @@ onBeforeUnmount(() => {
<div> <div>
<div v-if="!isMobile" class="left"> <div v-if="!isMobile" class="left">
<div style="margin-bottom: 10px;"> <div style="margin-bottom: 10px;">
<n-space v-if="multiActionMode"> <n-space v-if="multiActionMode" align="center">
<n-button @click="multiActionModeClick(false)" tertiary> <n-button @click="multiActionModeClick(false)" tertiary>
{{ t('cancelMultiAction') }} {{ t('cancelMultiAction') }}
</n-button> </n-button>
@@ -393,7 +421,7 @@ onBeforeUnmount(() => {
{{ t('downloadMail') }} {{ t('downloadMail') }}
</n-button> </n-button>
</n-space> </n-space>
<n-space v-else> <n-space v-else align="center">
<n-button @click="multiActionModeClick(true)" type="primary" tertiary> <n-button @click="multiActionModeClick(true)" type="primary" tertiary>
{{ t('multiAction') }} {{ t('multiAction') }}
</n-button> </n-button>
@@ -410,6 +438,9 @@ onBeforeUnmount(() => {
<n-button @click="backFirstPageAndRefresh" type="primary" tertiary> <n-button @click="backFirstPageAndRefresh" type="primary" tertiary>
{{ t('refresh') }} {{ t('refresh') }}
</n-button> </n-button>
<n-input v-if="showFilterInput" v-model:value="localFilterKeyword"
:placeholder="t('keywordQueryTip')" style="width: 200px; display: flex; align-items: center;"
clearable />
</n-space> </n-space>
</div> </div>
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize" <n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
@@ -482,10 +513,8 @@ onBeforeUnmount(() => {
</n-split> </n-split>
</div> </div>
<div class="left" v-else> <div class="left" v-else>
<n-space justify="center"> <n-space justify="space-around" align="center" :wrap="false" style="display: flex; align-items: center;">
<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>
<n-switch v-model:value="autoRefresh" size="small" :round="false"> <n-switch v-model:value="autoRefresh" size="small" :round="false">
<template #checked> <template #checked>
{{ t('refreshAfter', { msg: autoRefreshInterval }) }} {{ t('refreshAfter', { msg: autoRefreshInterval }) }}
@@ -498,6 +527,10 @@ onBeforeUnmount(() => {
{{ t('refresh') }} {{ t('refresh') }}
</n-button> </n-button>
</n-space> </n-space>
<div v-if="showFilterInput" style="padding: 0 10px; margin-top: 8px;">
<n-input v-model:value="localFilterKeyword"
:placeholder="t('keywordQueryTip')" size="small" clearable />
</div>
<div style="overflow: auto; height: 80vh;"> <div style="overflow: auto; height: 80vh;">
<n-list hoverable clickable> <n-list hoverable clickable>
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"> <n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
+1 -1
View File
@@ -158,7 +158,7 @@ onMounted(() => {
</div> </div>
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled" <MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled"
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
:fetchMailData="fetchMailData" :deleteMail="deleteMail" /> :fetchMailData="fetchMailData" :deleteMail="deleteMail" :showFilterInput="true" />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="sendbox" :tab="t('sendbox')"> <n-tab-pane name="sendbox" :tab="t('sendbox')">
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" <SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
+2 -8
View File
@@ -12,23 +12,19 @@ const { t } = useI18n({
messages: { messages: {
en: { en: {
addressQueryTip: 'Leave blank to query all addresses', addressQueryTip: 'Leave blank to query all addresses',
keywordQueryTip: 'Leave blank to not query by keyword',
query: 'Query', query: 'Query',
}, },
zh: { zh: {
addressQueryTip: '留空查询所有地址', addressQueryTip: '留空查询所有地址',
keywordQueryTip: '留空不按关键字查询',
query: '查询', query: '查询',
} }
} }
}); });
const mailBoxKey = ref("") const mailBoxKey = ref("")
const mailKeyword = ref("")
const queryMail = () => { const queryMail = () => {
adminMailTabAddress.value = adminMailTabAddress.value.trim(); adminMailTabAddress.value = adminMailTabAddress.value.trim();
mailKeyword.value = mailKeyword.value.trim();
mailBoxKey.value = Date.now(); mailBoxKey.value = Date.now();
} }
@@ -38,7 +34,6 @@ const fetchMailData = async (limit, offset) => {
+ `?limit=${limit}` + `?limit=${limit}`
+ `&offset=${offset}` + `&offset=${offset}`
+ (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '') + (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '')
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
); );
} }
@@ -51,14 +46,13 @@ const deleteMail = async (curMailId) => {
<div style="margin-top: 10px;"> <div style="margin-top: 10px;">
<n-input-group> <n-input-group>
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" <n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')"
@keydown.enter="queryMail" /> @keydown.enter="queryMail" clearable />
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
<n-button @click="queryMail" type="primary" tertiary> <n-button @click="queryMail" type="primary" tertiary>
{{ t('query') }} {{ t('query') }}
</n-button> </n-button>
</n-input-group> </n-input-group>
<div style="margin-top: 10px;"></div> <div style="margin-top: 10px;"></div>
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData" <MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData"
:deleteMail="deleteMail" /> :deleteMail="deleteMail" :showFilterInput="true" />
</div> </div>
</template> </template>
+1 -7
View File
@@ -10,12 +10,10 @@ const { t } = useI18n({
messages: { messages: {
en: { en: {
addressQueryTip: 'Leave blank to query all addresses', addressQueryTip: 'Leave blank to query all addresses',
keywordQueryTip: 'Leave blank to not query by keyword',
query: 'Query', query: 'Query',
}, },
zh: { zh: {
addressQueryTip: '留空查询所有地址', addressQueryTip: '留空查询所有地址',
keywordQueryTip: '留空不按关键字查询',
query: '查询', query: '查询',
} }
} }
@@ -23,12 +21,10 @@ const { t } = useI18n({
const mailBoxKey = ref("") const mailBoxKey = ref("")
const addressFilter = ref(); const addressFilter = ref();
const mailKeyword = ref("")
const addressFilterOptions = ref([]); const addressFilterOptions = ref([]);
const queryMail = () => { const queryMail = () => {
addressFilter.value = addressFilter.value ? addressFilter.value.trim() : addressFilter.value; addressFilter.value = addressFilter.value ? addressFilter.value.trim() : addressFilter.value;
mailKeyword.value = mailKeyword.value.trim();
mailBoxKey.value = Date.now(); mailBoxKey.value = Date.now();
} }
@@ -38,7 +34,6 @@ const fetchMailData = async (limit, offset) => {
+ `?limit=${limit}` + `?limit=${limit}`
+ `&offset=${offset}` + `&offset=${offset}`
+ (addressFilter.value ? `&address=${addressFilter.value}` : '') + (addressFilter.value ? `&address=${addressFilter.value}` : '')
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
); );
} }
@@ -77,13 +72,12 @@ onMounted(() => {
<n-input-group> <n-input-group>
<n-select v-model:value="addressFilter" :options="addressFilterOptions" clearable <n-select v-model:value="addressFilter" :options="addressFilterOptions" clearable
:placeholder="t('addressQueryTip')" /> :placeholder="t('addressQueryTip')" />
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
<n-button @click="queryMail" type="primary" tertiary> <n-button @click="queryMail" type="primary" tertiary>
{{ t('query') }} {{ t('query') }}
</n-button> </n-button>
</n-input-group> </n-input-group>
<div style="margin-top: 10px;"></div> <div style="margin-top: 10px;"></div>
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData" <MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData"
:deleteMail="deleteMail" /> :deleteMail="deleteMail" :showFilterInput="true" />
</div> </div>
</template> </template>
@@ -19,7 +19,7 @@ res = requests.get(
## Admin Mail API ## Admin Mail API
Supports `address` filter and `keyword` filter Supports `address` filter
```python ```python
import requests import requests
@@ -29,9 +29,8 @@ url = "https://<your-worker-address>/admin/mails"
querystring = { querystring = {
"limit":"20", "limit":"20",
"offset":"0", "offset":"0",
# address and keyword are optional parameters # address is optional parameter
"address":"xxxx@awsl.uk", "address":"xxxx@awsl.uk"
"keyword":"xxxx"
} }
headers = {"x-admin-auth": "<your-Admin-password>"} headers = {"x-admin-auth": "<your-Admin-password>"}
@@ -41,9 +40,11 @@ response = requests.get(url, headers=headers, params=querystring)
print(response.json()) print(response.json())
``` ```
**Note**: Keyword filtering has been removed from the backend API. If you need to filter emails by content, please use the frontend filter input in the UI, which filters the currently displayed page.
## User Mail API ## User Mail API
Supports `address` filter and `keyword` filter Supports `address` filter
```python ```python
import requests import requests
@@ -53,9 +54,8 @@ url = "https://<your-worker-address>/user_api/mails"
querystring = { querystring = {
"limit":"20", "limit":"20",
"offset":"0", "offset":"0",
# address and keyword are optional parameters # address is optional parameter
"address":"xxxx@awsl.uk", "address":"xxxx@awsl.uk"
"keyword":"xxxx"
} }
headers = {"x-admin-auth": "<your-Admin-password>"} headers = {"x-admin-auth": "<your-Admin-password>"}
@@ -64,3 +64,5 @@ response = requests.get(url, headers=headers, params=querystring)
print(response.json()) print(response.json())
``` ```
**Note**: Keyword filtering has been removed from the backend API. If you need to filter emails by content, please use the frontend filter input in the UI, which filters the currently displayed page.
@@ -19,7 +19,7 @@ res = requests.get(
## admin 邮件 API ## admin 邮件 API
支持 `address` filter 和 `keyword` filter 支持 `address` 过滤
```python ```python
import requests import requests
@@ -29,9 +29,8 @@ url = "https://<你的worker地址>/admin/mails"
querystring = { querystring = {
"limit":"20", "limit":"20",
"offset":"0", "offset":"0",
# adress 和 keyword 为可选参数 # address 为可选参数
"address":"xxxx@awsl.uk", "address":"xxxx@awsl.uk"
"keyword":"xxxx"
} }
headers = {"x-admin-auth": "<你的Admin密码>"} headers = {"x-admin-auth": "<你的Admin密码>"}
@@ -41,9 +40,11 @@ response = requests.get(url, headers=headers, params=querystring)
print(response.json()) print(response.json())
``` ```
**注意**:后端 API 已移除关键词过滤功能。如需按内容过滤邮件,请使用前端界面的过滤输入框,该功能可过滤当前显示的页面。
## user 邮件 API ## user 邮件 API
支持 `address` filter 和 `keyword` filter 支持 `address` 过滤
```python ```python
import requests import requests
@@ -53,9 +54,8 @@ url = "https://<你的worker地址>/user_api/mails"
querystring = { querystring = {
"limit":"20", "limit":"20",
"offset":"0", "offset":"0",
# adress 和 keyword 为可选参数 # address 为可选参数
"address":"xxxx@awsl.uk", "address":"xxxx@awsl.uk"
"keyword":"xxxx"
} }
headers = {"x-admin-auth": "<你的Admin密码>"} headers = {"x-admin-auth": "<你的Admin密码>"}
@@ -64,3 +64,5 @@ response = requests.get(url, headers=headers, params=querystring)
print(response.json()) print(response.json())
``` ```
**注意**:后端 API 已移除关键词过滤功能。如需按内容过滤邮件,请使用前端界面的过滤输入框,该功能可过滤当前显示的页面。
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "1.2.0", "version": "1.2.0",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^25.0.0", "@types/node": "^25.0.2",
"vitepress": "^1.6.4", "vitepress": "^1.6.4",
"wrangler": "^4.54.0" "wrangler": "^4.54.0"
}, },
+26 -26
View File
@@ -13,11 +13,11 @@ importers:
version: 3.10.1 version: 3.10.1
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^25.0.0 specifier: ^25.0.2
version: 25.0.0 version: 25.0.2
vitepress: vitepress:
specifier: ^1.6.4 specifier: ^1.6.4
version: 1.6.4(@algolia/client-search@5.46.0)(@types/node@25.0.0)(postcss@8.5.6)(search-insights@2.13.0)(typescript@5.4.5) version: 1.6.4(@algolia/client-search@5.46.0)(@types/node@25.0.2)(postcss@8.5.6)(search-insights@2.13.0)(typescript@5.4.5)
wrangler: wrangler:
specifier: ^4.54.0 specifier: ^4.54.0
version: 4.54.0 version: 4.54.0
@@ -484,8 +484,8 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@iconify-json/simple-icons@1.2.62': '@iconify-json/simple-icons@1.2.63':
resolution: {integrity: sha512-GpWQ294d4lraB3D2eBSSMROh1x9uKgpmyereLlGzVQjGZ7lbeFzby2ywXxyp4vEODmTDyf1/4WcOYs/yH4rJ5Q==} resolution: {integrity: sha512-xZl2UWCwE58VlqZ+pDPmaUhE2tq8MVSTJRr4/9nzzHlDdjJ0Ud1VxNXPrwTSgESKY29iCQw3S0r2nJTSNNngHw==}
'@iconify/types@2.0.0': '@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -605,14 +605,14 @@ packages:
'@jridgewell/trace-mapping@0.3.9': '@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@poppinss/colors@4.1.5': '@poppinss/colors@4.1.6':
resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==} resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
'@poppinss/dumper@0.6.5': '@poppinss/dumper@0.6.5':
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
'@poppinss/exception@1.2.2': '@poppinss/exception@1.2.3':
resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
'@rollup/rollup-android-arm-eabi@4.53.3': '@rollup/rollup-android-arm-eabi@4.53.3':
resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==}
@@ -773,8 +773,8 @@ packages:
'@types/mdurl@2.0.0': '@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/node@25.0.0': '@types/node@25.0.2':
resolution: {integrity: sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==} resolution: {integrity: sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==}
'@types/unist@3.0.3': '@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@@ -1645,7 +1645,7 @@ snapshots:
'@esbuild/win32-x64@0.27.0': '@esbuild/win32-x64@0.27.0':
optional: true optional: true
'@iconify-json/simple-icons@1.2.62': '@iconify-json/simple-icons@1.2.63':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
@@ -1735,17 +1735,17 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@poppinss/colors@4.1.5': '@poppinss/colors@4.1.6':
dependencies: dependencies:
kleur: 4.1.5 kleur: 4.1.5
'@poppinss/dumper@0.6.5': '@poppinss/dumper@0.6.5':
dependencies: dependencies:
'@poppinss/colors': 4.1.5 '@poppinss/colors': 4.1.6
'@sindresorhus/is': 7.1.1 '@sindresorhus/is': 7.1.1
supports-color: 10.2.2 supports-color: 10.2.2
'@poppinss/exception@1.2.2': {} '@poppinss/exception@1.2.3': {}
'@rollup/rollup-android-arm-eabi@4.53.3': '@rollup/rollup-android-arm-eabi@4.53.3':
optional: true optional: true
@@ -1876,7 +1876,7 @@ snapshots:
'@types/mdurl@2.0.0': {} '@types/mdurl@2.0.0': {}
'@types/node@25.0.0': '@types/node@25.0.2':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
@@ -1886,9 +1886,9 @@ snapshots:
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.0.0))(vue@3.5.25(typescript@5.4.5))': '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.0.2))(vue@3.5.25(typescript@5.4.5))':
dependencies: dependencies:
vite: 5.4.21(@types/node@25.0.0) vite: 5.4.21(@types/node@25.0.2)
vue: 3.5.25(typescript@5.4.5) vue: 3.5.25(typescript@5.4.5)
'@vue/compiler-core@3.5.25': '@vue/compiler-core@3.5.25':
@@ -2440,25 +2440,25 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.3 vfile-message: 4.0.3
vite@5.4.21(@types/node@25.0.0): vite@5.4.21(@types/node@25.0.2):
dependencies: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
postcss: 8.5.6 postcss: 8.5.6
rollup: 4.53.3 rollup: 4.53.3
optionalDependencies: optionalDependencies:
'@types/node': 25.0.0 '@types/node': 25.0.2
fsevents: 2.3.3 fsevents: 2.3.3
vitepress@1.6.4(@algolia/client-search@5.46.0)(@types/node@25.0.0)(postcss@8.5.6)(search-insights@2.13.0)(typescript@5.4.5): vitepress@1.6.4(@algolia/client-search@5.46.0)(@types/node@25.0.2)(postcss@8.5.6)(search-insights@2.13.0)(typescript@5.4.5):
dependencies: dependencies:
'@docsearch/css': 3.8.2 '@docsearch/css': 3.8.2
'@docsearch/js': 3.8.2(@algolia/client-search@5.46.0)(search-insights@2.13.0) '@docsearch/js': 3.8.2(@algolia/client-search@5.46.0)(search-insights@2.13.0)
'@iconify-json/simple-icons': 1.2.62 '@iconify-json/simple-icons': 1.2.63
'@shikijs/core': 2.5.0 '@shikijs/core': 2.5.0
'@shikijs/transformers': 2.5.0 '@shikijs/transformers': 2.5.0
'@shikijs/types': 2.5.0 '@shikijs/types': 2.5.0
'@types/markdown-it': 14.1.2 '@types/markdown-it': 14.1.2
'@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@25.0.0))(vue@3.5.25(typescript@5.4.5)) '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@25.0.2))(vue@3.5.25(typescript@5.4.5))
'@vue/devtools-api': 7.7.9 '@vue/devtools-api': 7.7.9
'@vue/shared': 3.5.25 '@vue/shared': 3.5.25
'@vueuse/core': 12.8.2(typescript@5.4.5) '@vueuse/core': 12.8.2(typescript@5.4.5)
@@ -2467,7 +2467,7 @@ snapshots:
mark.js: 8.11.1 mark.js: 8.11.1
minisearch: 7.2.0 minisearch: 7.2.0
shiki: 2.5.0 shiki: 2.5.0
vite: 5.4.21(@types/node@25.0.0) vite: 5.4.21(@types/node@25.0.2)
vue: 3.5.25(typescript@5.4.5) vue: 3.5.25(typescript@5.4.5)
optionalDependencies: optionalDependencies:
postcss: 8.5.6 postcss: 8.5.6
@@ -2536,12 +2536,12 @@ snapshots:
youch-core@0.3.3: youch-core@0.3.3:
dependencies: dependencies:
'@poppinss/exception': 1.2.2 '@poppinss/exception': 1.2.3
error-stack-parser-es: 1.0.5 error-stack-parser-es: 1.0.5
youch@4.1.0-beta.10: youch@4.1.0-beta.10:
dependencies: dependencies:
'@poppinss/colors': 4.1.5 '@poppinss/colors': 4.1.6
'@poppinss/dumper': 0.6.5 '@poppinss/dumper': 0.6.5
'@speed-highlight/core': 1.2.12 '@speed-highlight/core': 1.2.12
cookie: 1.1.1 cookie: 1.1.1
+3 -3
View File
@@ -11,10 +11,10 @@
"build": "wrangler deploy --dry-run --outdir dist --minify" "build": "wrangler deploy --dry-run --outdir dist --minify"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20251211.0", "@cloudflare/workers-types": "^4.20251213.0",
"@eslint/js": "9.39.1", "@eslint/js": "9.39.1",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@types/node": "^25.0.0", "@types/node": "^25.0.2",
"eslint": "9.39.1", "eslint": "9.39.1",
"globals": "^16.5.0", "globals": "^16.5.0",
"typescript-eslint": "^8.49.0", "typescript-eslint": "^8.49.0",
@@ -24,7 +24,7 @@
"@aws-sdk/client-s3": "3.888.0", "@aws-sdk/client-s3": "3.888.0",
"@aws-sdk/s3-request-presigner": "3.888.0", "@aws-sdk/s3-request-presigner": "3.888.0",
"@simplewebauthn/server": "10.0.1", "@simplewebauthn/server": "10.0.1",
"hono": "^4.10.8", "hono": "^4.11.0",
"jsonpath-plus": "^10.3.0", "jsonpath-plus": "^10.3.0",
"mimetext": "^3.0.27", "mimetext": "^3.0.27",
"postal-mime": "^2.6.1", "postal-mime": "^2.6.1",
+284 -378
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -3,14 +3,12 @@ import { handleListQuery } from "../common";
export default { export default {
getMails: async (c: Context<HonoCustomType>) => { getMails: async (c: Context<HonoCustomType>) => {
const { address, limit, offset, keyword } = c.req.query(); const { address, limit, offset } = c.req.query();
const addressQuery = address ? `address = ?` : ""; const addressQuery = address ? `address = ?` : "";
const addressParams = address ? [address] : []; const addressParams = address ? [address] : [];
const keywordQuery = keyword ? `raw like ?` : ""; const filterQuerys = [addressQuery].filter((item) => item).join(" and ");
const keywordParams = keyword ? [`%${keyword}%`] : [];
const filterQuerys = [addressQuery, keywordQuery].filter((item) => item).join(" and ");
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : ""; const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : "";
const filterParams = [...addressParams, ...keywordParams] const filterParams = [...addressParams]
return await handleListQuery(c, return await handleListQuery(c,
`SELECT * FROM raw_mails ${finalQuery}`, `SELECT * FROM raw_mails ${finalQuery}`,
`SELECT count(*) as count FROM raw_mails ${finalQuery}`, `SELECT count(*) as count FROM raw_mails ${finalQuery}`,
+3 -5
View File
@@ -5,22 +5,20 @@ import UserBindAddressModule from "./bind_address";
export default { export default {
getMails: async (c: Context<HonoCustomType>) => { getMails: async (c: Context<HonoCustomType>) => {
const { user_id } = c.get("userPayload"); const { user_id } = c.get("userPayload");
const { address, limit, offset, keyword } = c.req.query(); const { address, limit, offset } = c.req.query();
const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id); const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id);
const addressList = address ? bindedAddressList.filter((item) => item == address) : bindedAddressList; const addressList = address ? bindedAddressList.filter((item) => item == address) : bindedAddressList;
const addressQuery = `address IN (${addressList.map(() => "?").join(",")})`; const addressQuery = `address IN (${addressList.map(() => "?").join(",")})`;
const addressParams = addressList; const addressParams = addressList;
const keywordQuery = keyword ? `raw like ?` : "";
const keywordParams = keyword ? [`%${keyword}%`] : [];
// user must have at least one binded address to query mails // user must have at least one binded address to query mails
if (addressList.length <= 0) { if (addressList.length <= 0) {
return c.json({ results: [], count: 0 }); return c.json({ results: [], count: 0 });
} }
const filterQuerys = [addressQuery, keywordQuery].filter((item) => item).join(" and "); const filterQuerys = [addressQuery].filter((item) => item).join(" and ");
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : ""; const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : "";
const filterParams = [...addressParams, ...keywordParams] const filterParams = [...addressParams]
return await handleListQuery(c, return await handleListQuery(c,
`SELECT * FROM raw_mails ${finalQuery}`, `SELECT * FROM raw_mails ${finalQuery}`,
`SELECT count(*) as count FROM raw_mails ${finalQuery}`, `SELECT count(*) as count FROM raw_mails ${finalQuery}`,