From e390b1b1eb50a4e6c48aece834109f65188b0d74 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Fri, 30 Oct 2026 17:43:07 +0800 Subject: [PATCH] feat: mail find reply and outlook message retrieval --- package-lock.json | 544 +++++++++++++++++++++ package.json | 2 + src/main/capabilities/message-recv.test.ts | 67 +++ src/main/capabilities/message.ts | 130 +++++ 4 files changed, 743 insertions(+) create mode 100644 src/main/capabilities/message-recv.test.ts diff --git a/package-lock.json b/package-lock.json index fa46cc1..72bf83d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,9 @@ "adm-zip": "^0.5.16", "basic-ftp": "^6.2.3", "exceljs": "^4.4.0", + "imapflow": "^2.10.0", "lucide-react": "^0.468.0", + "mailparser": "^3.14.1", "nodemailer": "^6.9.16", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.10.38", @@ -2157,6 +2159,12 @@ "dev": true, "license": "0BSD" }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.15", "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", @@ -2769,6 +2777,22 @@ "win32" ] }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.12.0.tgz", + "integrity": "sha512-oELmoyA6ML9jDRMV3kgcMQFKxUfBU0yFVn6yTctVaLT5ygXnxH52I3TZEgV9EhXJC68/uFvE5Daj1/25c0Xa/A==", + "license": "MIT", + "dependencies": { + "domelementtype": "~2.3.0", + "domhandler": "~5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + }, + "peerDependencies": { + "selderee": "~0.12.0" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz", @@ -3255,6 +3279,20 @@ "d3-zoom": "^3.0.0" } }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.29", + "resolved": "https://registry.npmmirror.com/@zone-eu/mailsplit/-/mailsplit-5.4.29.tgz", + "integrity": "sha512-CCUNycmUjrV5M0wNf23waWAt9eOHczhjLbUQS7k0FNJvR0py9VSqHFt+tSl/D0+XI73VLG0U+PwYtq9R0zsm9Q==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.4.1", + "libmime": "5.6.3", + "libqp": "3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/abbrev": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", @@ -3819,6 +3857,15 @@ "node": ">= 4.0.0" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4711,6 +4758,25 @@ "node": ">=6" } }, + "node_modules/deepmerge-ts": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", + "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -4821,6 +4887,61 @@ "node": ">=12" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -5212,6 +5333,15 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding-japanese": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/encoding-japanese/-/encoding-japanese-2.4.0.tgz", + "integrity": "sha512-PE0KjlsDy9JSsoU4C2Q8NXb1kC4c+bZRac9ZvHXUE4q1Ht63gScKMdEy9jst9+Fi3M4eDkqzkEhBZvix4blP2A==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -5221,6 +5351,18 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", @@ -6058,6 +6200,15 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmmirror.com/html-entities/-/html-entities-2.6.0.tgz", @@ -6074,6 +6225,56 @@ ], "license": "MIT" }, + "node_modules/html-to-text": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/html-to-text/-/html-to-text-10.0.1.tgz", + "integrity": "sha512-GiVhRI1BatGARSCmlXWNCjDT0cWrwBWoeduLoV0WSKAgaV/wa+hUWy5LiQLUs4UwiUrE52ZCMfBGiKD87TDPrg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "~0.12.0", + "deepmerge-ts": "^8.0.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^10.1.0", + "selderee": "~0.12.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-cache-semantics": { "version": "4.3.0", "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", @@ -6123,6 +6324,22 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/idb-keyval": { "version": "6.3.0", "resolved": "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.3.0.tgz", @@ -6149,6 +6366,25 @@ ], "license": "BSD-3-Clause" }, + "node_modules/imapflow": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/imapflow/-/imapflow-2.10.0.tgz", + "integrity": "sha512-J4YH1eHwemE00xaCT2vn30s+81iJi+C4OHQEoO6/hptA/z75nGdP1isU3oDfMaiheHD240LIhMMJcMMCJ5RDYA==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.29", + "encoding-japanese": "2.4.0", + "iconv-lite": "0.7.3", + "libbase64": "1.4.1", + "libmime": "5.6.3", + "libqp": "3.0.1", + "pino": "10.3.1", + "socks": "2.8.10" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", @@ -6186,6 +6422,15 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.7.5", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6802,6 +7047,48 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/leac": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/leac/-/leac-0.7.0.tgz", + "integrity": "sha512-qMrZeyEekgdRQ9o6a4NAB2EQZrv827GJdn1vnapwSJ90hWRB4TzUSunvacPkxQ2TnNqHNI1/zSt0hlo0crG8Jw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/libbase64": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/libbase64/-/libbase64-1.4.1.tgz", + "integrity": "sha512-Nyh3H90B3gRtrcxIe56Ayy7635J0jmh6AbxbQgQae8wrSPQMH5wFqJChiMTB13yKG512L3YspbA5DzumhFTITA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/libmime": { + "version": "5.6.3", + "resolved": "https://registry.npmmirror.com/libmime/-/libmime-5.6.3.tgz", + "integrity": "sha512-cuTUW/Jybvh5O4/U9p61gyutHOjDtZyGQYuY99+bfnaFwqcDf2Nb+R6hMHY6DDen2H9wLKQSMpofWUgn2TI5JQ==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.4.0", + "iconv-lite": "0.7.3", + "libbase64": "1.4.1", + "libqp": "3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/libqp": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/libqp/-/libqp-3.0.1.tgz", + "integrity": "sha512-7QZxpPX25pga1WiMueduWxmrHMVBFrZPH6ZUaozGYa/H98JH6NVBSET3qVw+4NxzoraIW7rJ81YxSU9IalvCpg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", @@ -6811,6 +7098,25 @@ "immediate": "~3.0.5" } }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/listenercount/-/listenercount-1.0.1.tgz", @@ -6961,6 +7267,34 @@ "@jridgewell/sourcemap-codec": "^1.6.0" } }, + "node_modules/mailparser": { + "version": "3.14.1", + "resolved": "https://registry.npmmirror.com/mailparser/-/mailparser-3.14.1.tgz", + "integrity": "sha512-Z0LLk7F0yhSCx3xjyL8BMm6dXO3vuWrSAeVhm1F18wVGVBbLAKOsVsCjPyOhJy+KuyDW9PRyDuyiPb7yJCHwVA==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.29", + "he": "1.2.0", + "html-to-text": "10.0.1", + "libmime": "5.6.3", + "linkify-it": "5.0.2", + "nodemailer": "10.3.1", + "punycode.js": "2.3.1", + "tlds": "1.261.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "10.3.1", + "resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-10.3.1.tgz", + "integrity": "sha512-uF0AFsJC07Y1vdW7p580DKAnh7rAO+4UPSSB3mDCPNDufoiaOebV6JmrzpxB14OjEEEiBsNdYoM2Y+fN8M7BBA==", + "license": "MIT-0", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz", @@ -7320,6 +7654,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", @@ -7477,6 +7820,19 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/parseley": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/parseley/-/parseley-0.13.1.tgz", + "integrity": "sha512-uNBJZzmb60l6p6VWLTmevizNAGnE0xoSf1n0B4q3ntegDNzcS68NRCcBDZTcyXHxt2XhBChsCuqj4M+nChvE/A==", + "license": "MIT", + "dependencies": { + "leac": "^0.7.0", + "peberminta": "^0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7767,6 +8123,15 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/peberminta": { + "version": "0.10.0", + "resolved": "https://registry.npmmirror.com/peberminta/-/peberminta-0.10.0.tgz", + "integrity": "sha512-80B2AsU+I4Qdb0ZAPSfe9UwvGzwkM37IKIFEvdS3D/3Ndgv2bsuJ0bfG1+iEYO+l7Gfd4EUJmuRyq7efLgRMzQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", @@ -7794,6 +8159,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmmirror.com/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pkijs": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.3.tgz", @@ -7943,6 +8345,22 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/process-warning": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/process-warning/-/process-warning-5.1.1.tgz", + "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", @@ -7990,6 +8408,15 @@ "once": "^1.3.1" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pvtsutils": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", @@ -8017,6 +8444,12 @@ "node": ">=16.0.0" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz", @@ -8122,6 +8555,15 @@ "node": ">=10" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8411,6 +8853,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sanitize-filename": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", @@ -8452,6 +8909,18 @@ "loose-envify": "^1.1.0" } }, + "node_modules/selderee": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/selderee/-/selderee-0.12.0.tgz", + "integrity": "sha512-b1YMh3+DHZp59DLna3qVwQ5iOla/nrI6mLBNW02XxU77M3046Df6VLkoaJyFz20VsGIG5kkp+FK0kg4K4HnUFw==", + "license": "MIT", + "dependencies": { + "parseley": "~0.13.1" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", @@ -8707,6 +9176,39 @@ "node": ">=10" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8738,6 +9240,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -9026,6 +9537,24 @@ "integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==", "license": "Apache-2.0" }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -9113,6 +9642,15 @@ "node": ">=14.0.0" } }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmmirror.com/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz", @@ -9296,6 +9834,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/package.json b/package.json index 3bbdfaf..f7ec164 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "adm-zip": "^0.5.16", "basic-ftp": "^6.2.3", "exceljs": "^4.4.0", + "imapflow": "^2.10.0", "lucide-react": "^0.468.0", + "mailparser": "^3.14.1", "nodemailer": "^6.9.16", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.10.38", diff --git a/src/main/capabilities/message-recv.test.ts b/src/main/capabilities/message-recv.test.ts new file mode 100644 index 0000000..b996ae3 --- /dev/null +++ b/src/main/capabilities/message-recv.test.ts @@ -0,0 +1,67 @@ +import net from "node:net"; +import { afterEach, describe, expect, it } from "vitest"; +import type { CatalogComponent, FlowNode } from "../../shared/types"; +import type { RunContext } from "../engine/types"; +import { getHandler, hasCapability } from "../engine/registry"; +import "./message"; + +const context = {} as RunContext; +const component = {} as CatalogComponent; +const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} }); +const ids = [ + "Mail_FindMail", "Mail_FindMail_v1", "Mail_FindMail_v2", + "Mail_ReplyMail", "Mail_ReplyMail_v1", "Mail_ReplyMail_v2", + "Mail_DoGetMailMessage", "Mail_DoGetMailMessage_v1", "Mail_DoGetMailMessage_v2" +]; + +afterEach(async () => {}); + +describe("mail receive and reply", () => { + it("registers find, reply and Outlook message capabilities", () => { + for (const id of ids) expect(hasCapability(id), id).toBe(true); + }); + + it("replies through SMTP with the selected mail sender", async () => { + let received = ""; + const server = net.createServer((socket) => { + socket.setEncoding("utf8"); + socket.write("220 localhost ESMTP\r\n"); + let buffer = ""; + let dataMode = false; + socket.on("data", (chunk) => { + buffer += chunk; + const lines = buffer.split("\r\n"); + buffer = lines.pop() || ""; + for (const line of lines) { + if (dataMode) { + if (line === ".") { + dataMode = false; + socket.write("250 queued\r\n"); + } else received += line + "\n"; + } else if (/^EHLO/i.test(line)) socket.write("250-localhost\r\n250 OK\r\n"); + else if (/^MAIL FROM/i.test(line)) socket.write("250 ok\r\n"); + else if (/^RCPT TO/i.test(line)) socket.write("250 ok\r\n"); + else if (/^DATA/i.test(line)) { dataMode = true; socket.write("354 end with dot\r\n"); } + else if (/^QUIT/i.test(line)) { socket.write("221 bye\r\n"); socket.end(); } + else socket.write("250 OK\r\n"); + } + }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const port = (server.address() as net.AddressInfo).port; + try { + const handler = getHandler("Mail_ReplyMail_v2"); + expect(handler).toBeTruthy(); + const result = await handler!(context, node("Mail_ReplyMail_v2"), { + account_obj: { host: "127.0.0.1", port, account: "robot@example.com", secure: "False" }, + mail_obj: { from: "owner@example.com", subject: "status", messageId: "" }, + content: "reply delivered" + }, component); + expect(result).toMatchObject({ accepted: ["owner@example.com"] }); + expect(received).toContain("reply delivered"); + expect(received).toContain("Re: status"); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } + }, 30000); +}); diff --git a/src/main/capabilities/message.ts b/src/main/capabilities/message.ts index 3d32714..b083d6a 100644 --- a/src/main/capabilities/message.ts +++ b/src/main/capabilities/message.ts @@ -1,5 +1,7 @@ import nodemailer from "nodemailer"; import { register } from "../engine/registry"; +import { RuntimeError } from "../engine/types"; +import { powershell } from "./desktop"; function text(args: Record, keys: string[], fallback = ""): string { for (const key of keys) if (args[key] !== undefined && args[key] !== null && args[key] !== "") return String(args[key]); @@ -42,3 +44,131 @@ register(["Mail_Send", "Mail_Send_V1", "Mail_UnionSend", "Mail_UnionSend_v1"], a transporter.close(); } }); + +export interface NormalizedMail { + uid: string; from: string; to: string[]; cc: string[]; subject: string; + text: string; html: string; date: string; messageId: string; hasAttachment: boolean; + attachmentNames: string[]; +} + +export function normalizeAccount(args: Record): Record { + return accountObject(args.account_obj ?? args.accountObj ?? args.account); +} + +function addressList(value: unknown): string[] { + if (!value) return []; + if (Array.isArray(value)) return value.map((item) => String(typeof item === "object" && item && "text" in (item as Record) ? (item as { text: unknown }).text : item)).filter(Boolean); + if (typeof value === "object") { const record = value as { text?: unknown; value?: unknown }; return addressList(record.text ?? record.value); } + return String(value).split(/[;,,;\s]+/).filter(Boolean); +} + +export function normalizeMail(input: Record, uid = ""): NormalizedMail { + const attachments = Array.isArray(input.attachments) ? input.attachments as Array> : []; + return { + uid: String(input.uid ?? uid), + from: String(input.from ?? input.sender ?? ""), + to: addressList(input.to), cc: addressList(input.cc), + subject: String(input.subject ?? ""), + text: String(input.text ?? input.body ?? ""), + html: String(input.html ?? ""), + date: input.date instanceof Date ? input.date.toISOString() : String(input.date ?? input.receivedTime ?? ""), + messageId: String(input.messageId ?? ""), + hasAttachment: Boolean(input.hasAttachment ?? attachments.length), + attachmentNames: attachments.map((item) => String(item.filename ?? item.name ?? "")).filter(Boolean) + }; +} + +function matches(mail: NormalizedMail, args: Record): boolean { + const contains = (needle: unknown, haystack: string): boolean => !needle || haystack.toLowerCase().includes(String(needle).toLowerCase()); + return contains(args.From_data, mail.from) + && contains(args.To_data, mail.to.join(" ")) + && contains(args.Subject_data, mail.subject) + && contains(args.Body_data, mail.text + "\n" + mail.html) + && (args.attachment === undefined || String(args.attachment) === "2" || (String(args.attachment) === "1") === mail.hasAttachment); +} + +async function findImapMail(args: Record, node: { id: string; componentId: string }): Promise { + const configured = normalizeAccount(args); + const merged = { ...args, ...configured }; + const account = text(merged, ["account", "username", "user", "from"]); + const password = text(merged, ["password", "pass", "token"]); + const host = text(merged, ["imap", "imapHost", "imap_host", "server", "host"]); + if (!host || !account) throw new RuntimeError("validation_error", "IMAP 配置不完整:需要 host 和 account", { nodeId: node.id, componentId: node.componentId }); + const port = Number(text(merged, ["imapPort", "imap_port", "recvPort", "port"], "993")); + const secure = ["true", "1", "是", "True"].includes(text(merged, ["imap_ssl", "imapSsl", "is_ssl", "secure"], "True")); + const folder = text(args, ["folder_name", "email_folder", "folder"], "INBOX"); + const { ImapFlow } = await import("imapflow"); + const client = new ImapFlow({ host, port, secure, auth: { user: account, pass: password }, logger: false }); + const mails: NormalizedMail[] = []; + try { + await client.connect(); + const lock = await client.getMailboxLock(folder); + try { + const range = String(args.search_range ?? "ALL").toUpperCase(); + const criteria: Record = range === "UNSEEN" ? { seen: false } : range === "SEEN" ? { seen: true } : range === "FLAGGED" ? { flagged: true } : range === "ANSWERED" ? { answered: true } : range === "DELETED" ? { deleted: true } : range === "DRAFT" ? { draft: true } : { all: true }; + if (args.From_data) criteria.from = String(args.From_data); + if (args.To_data) criteria.to = String(args.To_data); + if (args.Subject_data) criteria.subject = String(args.Subject_data); + const uids = await client.search(criteria as never, { uid: true }) || []; + const selected = Math.max(1, Number(args.email_num ?? 1) || 1); + for (let index = uids.length - 1; index >= 0 && mails.length < Math.max(20, selected); index -= 1) { + const download = await client.fetchOne(String(uids[index]), { uid: true, envelope: true, flags: true, bodyStructure: true, source: true }, { uid: true }); + if (!download || !download.source) continue; + const { simpleParser } = await import("mailparser"); + const parsed = await simpleParser(download.source); + const mail = normalizeMail(parsed as unknown as Record, String(download.uid)); + if (matches(mail, args)) mails.push(mail); + } + } finally { lock.release(); } + } finally { client.close(); } + const selected = Math.max(1, Number(args.email_num ?? 1) || 1); + return mails.slice(0, selected); +} + +register(["Mail_FindMail", "Mail_FindMail_v1", "Mail_FindMail_v2"], async (_ctx, node, args) => findImapMail(args, node)); + +register(["Mail_ReplyMail", "Mail_ReplyMail_v1", "Mail_ReplyMail_v2"], async (_ctx, node, args) => { + const configured = accountObject(args.mail_obj ?? args.email_obj ?? args.account_obj); + const mail = configured && Object.keys(configured).length ? configured as unknown as NormalizedMail : args.mail as unknown as NormalizedMail; + const merged = { ...args, ...normalizeAccount(args) }; + const account = text(merged, ["account", "username", "user", "from"]); + const password = text(merged, ["password", "pass"]); + const host = text(merged, ["smtp", "smtpHost", "smtp_host", "server", "host"]); + if (!host || !account) throw new RuntimeError("validation_error", "SMTP 配置不完整:需要 host 和 account", { nodeId: node.id, componentId: node.componentId }); + const port = Number(text(merged, ["smtpPort", "smtp_port", "sendPort", "port"], "465")); + const secure = ["true", "1", "是", "True"].includes(text(merged, ["smtp_ssl", "smtpSsl", "is_ssl", "secure"], "True")); + const transporter = nodemailer.createTransport({ host, port, secure, auth: password ? { user: account, pass: password } : undefined }); + try { + const result = await transporter.sendMail({ + from: account, + to: mail?.from ? [mail.from] : list(text(args, ["to", "receiver"])), + subject: text(args, ["subject"], mail?.subject ? "Re: " + mail.subject : "Re:"), + text: text(args, ["content", "body"]), + inReplyTo: mail?.messageId, references: mail?.messageId, + attachments: text(args, ["attachment"]) ? [{ path: text(args, ["attachment"]) }] : undefined + }); + return { messageId: result.messageId, accepted: result.accepted }; + } finally { transporter.close(); } +}); + +function outlookScript(args: Record): string { + const criteria = Buffer.from(JSON.stringify({ to: String(args.To_data ?? ""), from: String(args.From_data ?? ""), subject: String(args.Subject_data ?? ""), body: String(args.Body_data ?? "") }), "utf8").toString("base64"); + return [ + "$outlook=New-Object -ComObject Outlook.Application;$ns=$outlook.GetNamespace('MAPI')", + `$folderPath=${JSON.stringify(String(args.folder_name || args.email_folder || "INBOX"))}`, + "$folder=$ns.GetDefaultFolder(6);foreach($part in ($folderPath -split '/')){ if($part -and $part -ne 'INBOX'){ $folder=$folder.Folders.Item($part) } }", + "$folder.Items.Sort('[ReceivedTime]',$true)|Out-Null;$rows=@();$count=[Math]::Min(500,$folder.Items.Count)", + `$criteria=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${criteria}'))|ConvertFrom-Json`, + "for($i=1;$i -le $count;$i++){ $item=$folder.Items.Item($i); $subject=[string]$item.Subject; $body=[string]$item.Body; $to=[string]$item.To; $from=[string]$item.SenderEmailAddress; if(($criteria.to -and $to -notlike ('*'+$criteria.to+'*')) -or ($criteria.from -and $from -notlike ('*'+$criteria.from+'*')) -or ($criteria.subject -and $subject -notlike ('*'+$criteria.subject+'*')) -or ($criteria.body -and $body -notlike ('*'+$criteria.body+'*'))){continue}; $bodyB64=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body)); $rows += @{uid=[string]$item.EntryID;from=$from;to=$to;subject=$subject;text=$bodyB64;html='';date=$item.ReceivedTime.ToString('o');messageId=[string]$item.EntryID;hasAttachment=($item.Attachments.Count -gt 0);attachmentNames=@()}; if($rows.Count -ge 100){break} }", + "$json=$rows|ConvertTo-Json -Compress -Depth 4;[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json))|Write-Output" + ].join(";"); +} + +register(["Mail_DoGetMailMessage", "Mail_DoGetMailMessage_v1", "Mail_DoGetMailMessage_v2"], async (_ctx, node, args) => { + const output = await powershell(outlookScript(args), 120000); + if (!output.trim()) throw new RuntimeError("outlook_unavailable", "Outlook COM 未返回数据;请确认 Outlook 已安装并至少配置一个邮箱", { nodeId: node.id, componentId: node.componentId }); + const rows = JSON.parse(Buffer.from(output.trim(), "base64").toString("utf8")); + const mails = (Array.isArray(rows) ? rows : [rows]).map((row: Record) => normalizeMail({ ...row, text: row.text ? Buffer.from(String(row.text), "base64").toString("utf8") : "" })); + const selected = Math.max(1, Number(args.email_num ?? 1) || 1); + return mails.slice(0, selected); +});