feat(history): complete messaging core before multi-account gateways
This commit is contained in:
Generated
+487
@@ -12,7 +12,9 @@
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"imapflow": "^1.6.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mailparser": "^3.9.14",
|
||||
"mammoth": "^1.9.0",
|
||||
"nodemailer": "^7.0.5",
|
||||
"pdf-parse": "^1.1.1",
|
||||
@@ -20,6 +22,28 @@
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
},
|
||||
"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/@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/@xmldom/xmldom": {
|
||||
"version": "0.8.13",
|
||||
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||
@@ -29,6 +53,17 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zone-eu/mailsplit": {
|
||||
"version": "5.4.14",
|
||||
"resolved": "https://registry.npmmirror.com/@zone-eu/mailsplit/-/mailsplit-5.4.14.tgz",
|
||||
"integrity": "sha512-rz0FQOhN3Vq1XrSeSSa9+dPcaFbBxmQPjiZm6zS9oxdVHV7rOWIAYX3yP2YAUf0qBncY8CI+NogzPCmMVrMXcw==",
|
||||
"license": "(MIT OR EUPL-1.1+)",
|
||||
"dependencies": {
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.4.1",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
|
||||
@@ -51,6 +86,15 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"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/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -244,6 +288,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge-ts": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||
@@ -259,6 +312,61 @@
|
||||
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"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/duck": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz",
|
||||
@@ -306,6 +414,27 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding-japanese": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
|
||||
"integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.10.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/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@@ -515,6 +644,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/helmet": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/helmet/-/helmet-8.3.0.tgz",
|
||||
@@ -527,6 +665,56 @@
|
||||
"url": "https://github.com/sponsors/EvanHahn"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-text": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/html-to-text/-/html-to-text-10.0.0.tgz",
|
||||
"integrity": "sha512-2OH59Gtprdczel+7Rxgpz9hGVJREaf8Lt1H4kZwWHpEn70VQKRuMNGsb2eDbwaTzrYzb0hheiOG1P7Dim0B4dQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@selderee/plugin-htmlparser2": "~0.12.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"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-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -563,6 +751,22 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/imapflow": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/imapflow/-/imapflow-1.6.1.tgz",
|
||||
"integrity": "sha512-lBkh0ZcKeYHDS3sWfxEcD7j24frrrn4wtbe0UUtLqdLwMSvm9Mygx8j7ZtzWgnvqI7V1DkNfcsidJ/qasb7FBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zone-eu/mailsplit": "5.4.14",
|
||||
"encoding-japanese": "2.2.0",
|
||||
"iconv-lite": "0.7.3",
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.4.1",
|
||||
"libqp": "2.1.1",
|
||||
"pino": "10.3.1",
|
||||
"socks": "2.8.9"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
|
||||
@@ -575,6 +779,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.3.1.tgz",
|
||||
"integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -651,6 +864,39 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"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.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/libbase64/-/libbase64-1.3.0.tgz",
|
||||
"integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/libmime": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/libmime/-/libmime-5.4.1.tgz",
|
||||
"integrity": "sha512-0wHGhsofo9IdQPenr3BBHXuxcwMq4atFUTsZ9Ogc1OvI5h4rUdDIrBQEN9JHjCXfDMrE59LUMJWsTD82wTYk8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encoding-japanese": "2.2.0",
|
||||
"iconv-lite": "0.7.3",
|
||||
"libbase64": "1.3.0",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/libqp": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/libqp/-/libqp-2.1.1.tgz",
|
||||
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
|
||||
@@ -660,6 +906,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/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -713,6 +978,33 @@
|
||||
"underscore": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser": {
|
||||
"version": "3.9.14",
|
||||
"resolved": "https://registry.npmmirror.com/mailparser/-/mailparser-3.9.14.tgz",
|
||||
"integrity": "sha512-3QD6TRXcyXtq2NCuyA2AEjqmallQkyxYmZI9GMCIvQDCaB9Uc034WUI1x8RUBYFnk5+p7h14JEz4O/lrxQmttw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zone-eu/mailsplit": "5.4.14",
|
||||
"encoding-japanese": "2.2.0",
|
||||
"he": "1.2.0",
|
||||
"html-to-text": "10.0.0",
|
||||
"iconv-lite": "0.7.3",
|
||||
"libmime": "5.4.1",
|
||||
"linkify-it": "5.0.2",
|
||||
"nodemailer": "9.0.3",
|
||||
"punycode.js": "2.3.1",
|
||||
"tlds": "1.261.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser/node_modules/nodemailer": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-9.0.3.tgz",
|
||||
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mammoth": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.12.0.tgz",
|
||||
@@ -847,6 +1139,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/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -880,6 +1181,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/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -924,6 +1238,15 @@
|
||||
"url": "https://github.com/sponsors/mehmet-kozan"
|
||||
}
|
||||
},
|
||||
"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/pg": {
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmmirror.com/pg/-/pg-8.22.0.tgz",
|
||||
@@ -1013,6 +1336,43 @@
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"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/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
@@ -1058,6 +1418,22 @@
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/process-warning/-/process-warning-5.0.0.tgz",
|
||||
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1071,6 +1447,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"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/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.3.tgz",
|
||||
@@ -1087,6 +1472,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"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/range-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.3.0.tgz",
|
||||
@@ -1136,6 +1527,15 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
|
||||
@@ -1172,12 +1572,33 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/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",
|
||||
@@ -1319,6 +1740,39 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"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.9",
|
||||
"resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.9.tgz",
|
||||
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||
"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/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
|
||||
@@ -1358,6 +1812,33 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/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/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1398,6 +1879,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"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/underscore": {
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.8.tgz",
|
||||
|
||||
+7
-3
@@ -5,18 +5,22 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"test": "node --test test/business.test.js"
|
||||
"test": "node --test test/business.test.js",
|
||||
"test:email-protocol": "node --test test/email-protocol.test.js",
|
||||
"test:email-api": "node --test test/email-api-e2e.test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"imapflow": "^1.6.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mailparser": "^3.9.14",
|
||||
"mammoth": "^1.9.0",
|
||||
"nodemailer": "^7.0.5",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pg": "^8.13.1"
|
||||
,"ws": "^8.18.3"
|
||||
"pg": "^8.13.1",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
|
||||
+41
-25
@@ -10,32 +10,48 @@ function parseJson(text) {
|
||||
export async function deepseek(system, user, json = false) {
|
||||
const apiKey = process.env.DEEPSEEK_API_KEY
|
||||
if (!apiKey) throw Object.assign(new Error('DEEPSEEK_API_KEY 尚未配置'), { status: 503 })
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), Number(process.env.AI_TIMEOUT_MS || 45000))
|
||||
try {
|
||||
const response = await fetch(`${(process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/anthropic').replace(/\/$/, '')}/v1/messages`, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash',
|
||||
max_tokens: Number(process.env.AI_MAX_TOKENS || 4096),
|
||||
temperature: json ? 0.1 : 0.3,
|
||||
system,
|
||||
messages: [{ role: 'user', content: [{ type: 'text', text: user }] }],
|
||||
}),
|
||||
})
|
||||
const body = await response.json()
|
||||
if (!response.ok) throw Object.assign(new Error(body?.error?.message || `AI 请求失败 (${response.status})`), { status: 502 })
|
||||
const text = contentText(body)
|
||||
return json ? parseJson(text) : text
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
const endpoint = `${(process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/anthropic').replace(/\/$/, '')}/v1/messages`
|
||||
const attempts = json ? 2 : 1
|
||||
let prompt = user
|
||||
let lastError
|
||||
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), Number(process.env.AI_TIMEOUT_MS || 45000))
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash',
|
||||
max_tokens: Number(process.env.AI_MAX_TOKENS || 4096),
|
||||
temperature: json ? 0 : 0.3,
|
||||
system,
|
||||
messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
||||
}),
|
||||
})
|
||||
const body = await response.json()
|
||||
if (!response.ok) {
|
||||
throw Object.assign(new Error(body?.error?.message || `AI 请求失败 (${response.status})`), { status: 502 })
|
||||
}
|
||||
const text = contentText(body)
|
||||
if (!json) return text
|
||||
try {
|
||||
return parseJson(text)
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
prompt = `${user}\n\n上一次响应不是合法 JSON。请重新生成完整结果:只允许输出一个可被 JSON.parse 直接解析的 JSON 值;不得使用 Markdown;字符串中的双引号、换行和反斜杠必须正确转义。`
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
throw Object.assign(new Error('AI 返回格式异常,自动重试后仍无法解析'), { status: 502, cause: lastError })
|
||||
}
|
||||
|
||||
export function conversationContext(messages) {
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import crypto from 'node:crypto'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { ImapFlow } from 'imapflow'
|
||||
import { simpleParser } from 'mailparser'
|
||||
|
||||
function encryptionKey() {
|
||||
const secret = process.env.EMAIL_CREDENTIAL_KEY || process.env.JWT_SECRET
|
||||
if (!secret) throw Object.assign(new Error('EMAIL_CREDENTIAL_KEY 或 JWT_SECRET 尚未配置'), { status: 503 })
|
||||
return crypto.createHash('sha256').update(secret).digest()
|
||||
}
|
||||
|
||||
export function encryptEmailPassword(password) {
|
||||
const iv = crypto.randomBytes(12)
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey(), iv)
|
||||
const encrypted = Buffer.concat([cipher.update(String(password), 'utf8'), cipher.final()])
|
||||
return `v1:${iv.toString('base64url')}:${cipher.getAuthTag().toString('base64url')}:${encrypted.toString('base64url')}`
|
||||
}
|
||||
|
||||
export function decryptEmailPassword(value) {
|
||||
const [version, ivValue, tagValue, encryptedValue] = String(value || '').split(':')
|
||||
if (version !== 'v1' || !ivValue || !tagValue || !encryptedValue) {
|
||||
throw Object.assign(new Error('Email 凭据无效,请重新连接邮箱'), { status: 409 })
|
||||
}
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', encryptionKey(), Buffer.from(ivValue, 'base64url'))
|
||||
decipher.setAuthTag(Buffer.from(tagValue, 'base64url'))
|
||||
return Buffer.concat([
|
||||
decipher.update(Buffer.from(encryptedValue, 'base64url')),
|
||||
decipher.final(),
|
||||
]).toString('utf8')
|
||||
}
|
||||
|
||||
function imapClient(channel, password) {
|
||||
return new ImapFlow({
|
||||
host: channel.imapHost,
|
||||
port: Number(channel.imapPort || 993),
|
||||
secure: channel.imapSecure !== false,
|
||||
auth: { user: channel.account, pass: password },
|
||||
logger: false,
|
||||
tls: { rejectUnauthorized: channel.allowInvalidCertificate !== true },
|
||||
})
|
||||
}
|
||||
|
||||
function smtpTransport(channel, password) {
|
||||
return nodemailer.createTransport({
|
||||
host: channel.smtpHost,
|
||||
port: Number(channel.smtpPort || 465),
|
||||
secure: channel.smtpSecure !== false,
|
||||
auth: { user: channel.account, pass: password },
|
||||
tls: { rejectUnauthorized: channel.allowInvalidCertificate !== true },
|
||||
})
|
||||
}
|
||||
|
||||
export async function verifyEmailConnection(channel, password) {
|
||||
const startedAt = Date.now()
|
||||
const imap = imapClient(channel, password)
|
||||
try {
|
||||
await imap.connect()
|
||||
await imap.mailboxOpen(channel.imapFolder || 'INBOX', { readOnly: true })
|
||||
} finally {
|
||||
if (imap.usable) await imap.logout().catch(() => undefined)
|
||||
}
|
||||
const smtp = smtpTransport(channel, password)
|
||||
await smtp.verify()
|
||||
smtp.close()
|
||||
return { latencyMs: Date.now() - startedAt }
|
||||
}
|
||||
|
||||
function address(value) {
|
||||
const item = value?.value?.[0]
|
||||
return {
|
||||
name: String(item?.name || item?.address || '').trim(),
|
||||
email: String(item?.address || '').trim().toLowerCase(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchEmailMessages(channel, { full = false } = {}) {
|
||||
const password = decryptEmailPassword(channel.encryptedPassword)
|
||||
const client = imapClient(channel, password)
|
||||
const messages = []
|
||||
try {
|
||||
await client.connect()
|
||||
await client.mailboxOpen(channel.imapFolder || 'INBOX')
|
||||
let uids
|
||||
if (!full && Number(channel.lastUid || 0) > 0) {
|
||||
uids = await client.search({ uid: `${Number(channel.lastUid) + 1}:*` }, { uid: true })
|
||||
} else {
|
||||
const days = Math.max(1, Math.min(365, Number(channel.syncDays || 30)))
|
||||
uids = await client.search({ since: new Date(Date.now() - days * 86400000) }, { uid: true })
|
||||
}
|
||||
uids = (uids || []).slice(-500)
|
||||
if (!uids.length) return { messages, lastUid: Number(channel.lastUid || 0) }
|
||||
|
||||
for await (const row of client.fetch(uids, {
|
||||
uid: true,
|
||||
envelope: true,
|
||||
flags: true,
|
||||
internalDate: true,
|
||||
source: true,
|
||||
}, { uid: true })) {
|
||||
const parsed = await simpleParser(row.source)
|
||||
const sender = address(parsed.from)
|
||||
const recipients = parsed.to?.value || []
|
||||
const attachments = (parsed.attachments || []).map((attachment, index) => ({
|
||||
id: `${row.uid}-${index + 1}`,
|
||||
name: attachment.filename || `attachment-${index + 1}`,
|
||||
mimeType: attachment.contentType || 'application/octet-stream',
|
||||
size: attachment.size || attachment.content?.length || 0,
|
||||
contentId: attachment.contentId,
|
||||
base64: attachment.content?.length <= 20 * 1024 * 1024
|
||||
? attachment.content.toString('base64')
|
||||
: undefined,
|
||||
}))
|
||||
messages.push({
|
||||
uid: Number(row.uid),
|
||||
messageId: String(parsed.messageId || `imap-${row.uid}`),
|
||||
inReplyTo: parsed.inReplyTo || undefined,
|
||||
references: Array.isArray(parsed.references) ? parsed.references : parsed.references ? [parsed.references] : [],
|
||||
subject: String(parsed.subject || '(无主题)').trim(),
|
||||
from: sender,
|
||||
to: recipients.map((item) => ({ name: item.name || item.address || '', email: String(item.address || '').toLowerCase() })),
|
||||
text: String(parsed.text || '').trim() || String(parsed.html || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(),
|
||||
html: typeof parsed.html === 'string' ? parsed.html : undefined,
|
||||
date: (parsed.date || row.internalDate || new Date()).toISOString(),
|
||||
seen: row.flags?.has('\\Seen') || false,
|
||||
attachments,
|
||||
})
|
||||
}
|
||||
return {
|
||||
messages: messages.sort((a, b) => Date.parse(a.date) - Date.parse(b.date)),
|
||||
lastUid: Math.max(Number(channel.lastUid || 0), ...messages.map((message) => message.uid)),
|
||||
}
|
||||
} finally {
|
||||
if (client.usable) await client.logout().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmailMessage(channel, {
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
inReplyTo,
|
||||
references,
|
||||
attachments,
|
||||
}) {
|
||||
const password = decryptEmailPassword(channel.encryptedPassword)
|
||||
const transport = smtpTransport(channel, password)
|
||||
try {
|
||||
return await transport.sendMail({
|
||||
from: channel.displayName ? { name: channel.displayName, address: channel.account } : channel.account,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
inReplyTo,
|
||||
references,
|
||||
attachments,
|
||||
})
|
||||
} finally {
|
||||
transport.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function markEmailUidsSeen(channel, uids) {
|
||||
const values = [...new Set(uids.map(Number).filter(Number.isFinite))]
|
||||
if (!values.length) return
|
||||
const password = decryptEmailPassword(channel.encryptedPassword)
|
||||
const client = imapClient(channel, password)
|
||||
try {
|
||||
await client.connect()
|
||||
await client.mailboxOpen(channel.imapFolder || 'INBOX')
|
||||
await client.messageFlagsAdd(values, ['\\Seen'], { uid: true })
|
||||
} finally {
|
||||
if (client.usable) await client.logout().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
+1070
-70
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { countBy, dateKey, percent, telegramAttachment } from '../src/business.js'
|
||||
import { decryptEmailPassword, encryptEmailPassword } from '../src/email.js'
|
||||
|
||||
test('Telegram 图片、文档、语音和贴纸映射为可下载文件', () => {
|
||||
assert.deepEqual(
|
||||
@@ -42,3 +43,12 @@ test('业务指标工具处理百分比、分组和无效日期', () => {
|
||||
{ new: 2, won: 1 },
|
||||
)
|
||||
})
|
||||
|
||||
test('Email 渠道凭据使用认证加密且可正确解密', () => {
|
||||
process.env.EMAIL_CREDENTIAL_KEY = 'lingxi-email-test-key-with-more-than-32-bytes'
|
||||
const encrypted = encryptEmailPassword('app-password-123')
|
||||
assert.notEqual(encrypted, 'app-password-123')
|
||||
assert.match(encrypted, /^v1:/)
|
||||
assert.equal(decryptEmailPassword(encrypted), 'app-password-123')
|
||||
assert.throws(() => decryptEmailPassword(`${encrypted.slice(0, -1)}x`))
|
||||
})
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
encryptEmailPassword,
|
||||
fetchEmailMessages,
|
||||
sendEmailMessage,
|
||||
} from '../src/email.js'
|
||||
|
||||
const enabled = process.env.RUN_EMAIL_API_E2E === '1'
|
||||
const baseUrl = process.env.EMAIL_API_E2E_BASE_URL || 'http://127.0.0.1:8088'
|
||||
|
||||
function localChannel(account) {
|
||||
return {
|
||||
account,
|
||||
encryptedPassword: encryptEmailPassword('pwd'),
|
||||
imapHost: '127.0.0.1',
|
||||
imapPort: 3143,
|
||||
imapSecure: false,
|
||||
imapFolder: 'INBOX',
|
||||
smtpHost: '127.0.0.1',
|
||||
smtpPort: 3025,
|
||||
smtpSecure: false,
|
||||
syncDays: 7,
|
||||
}
|
||||
}
|
||||
|
||||
function unwrap(payload) {
|
||||
return payload && typeof payload === 'object' && 'data' in payload ? payload.data : payload
|
||||
}
|
||||
|
||||
async function request(path, { token, method = 'GET', body } = {}) {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
const text = await response.text()
|
||||
const payload = text ? JSON.parse(text) : null
|
||||
if (!response.ok) throw new Error(`${method} ${path} -> ${response.status}: ${payload?.message || text}`)
|
||||
return unwrap(payload)
|
||||
}
|
||||
|
||||
async function waitForSenderMessage(predicate) {
|
||||
const sender = localChannel('sender@local.test')
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
const result = await fetchEmailMessages(sender, { full: true })
|
||||
const message = result.messages.find(predicate)
|
||||
if (message) return message
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
}
|
||||
throw new Error('等待网关发出的邮件进入 sender IMAP 超时')
|
||||
}
|
||||
|
||||
test('真实 HTTP 网关完成 Email 连接、全量同步、附件读取、回复与已读', { skip: !enabled }, async () => {
|
||||
process.env.JWT_SECRET ||= 'lingxi-email-api-e2e-secret'
|
||||
const suffix = Date.now()
|
||||
const senderChannel = localChannel('sender@local.test')
|
||||
const olderSubject = `Lingxi sorting older ${suffix}`
|
||||
const newerSubject = `Lingxi sorting newer ${suffix}`
|
||||
await sendEmailMessage(senderChannel, {
|
||||
to: 'receiver@local.test',
|
||||
subject: olderSubject,
|
||||
text: 'Older conversation used for stable ordering',
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 30))
|
||||
await sendEmailMessage(senderChannel, {
|
||||
to: 'receiver@local.test',
|
||||
subject: newerSubject,
|
||||
text: 'Newer conversation must remain above the opened older conversation',
|
||||
})
|
||||
|
||||
const session = await request('/api/auth/register', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: 'Email API E2E',
|
||||
account: `email-api-${suffix}@local.test`,
|
||||
password: 'Lingxi-Email-Api-2026!',
|
||||
company: 'Lingxi Protocol Lab',
|
||||
industry: '跨境电商',
|
||||
teamSize: '1-10人',
|
||||
},
|
||||
})
|
||||
assert.ok(session.token)
|
||||
|
||||
const connected = await request('/api/channels/email/connect', {
|
||||
token: session.token,
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: '本地协议邮箱',
|
||||
displayName: 'Lingxi Receiver',
|
||||
account: 'receiver@local.test',
|
||||
password: 'pwd',
|
||||
imapHost: 'host.docker.internal',
|
||||
imapPort: 3143,
|
||||
imapSecure: false,
|
||||
imapFolder: 'INBOX',
|
||||
smtpHost: 'host.docker.internal',
|
||||
smtpPort: 3025,
|
||||
smtpSecure: false,
|
||||
syncDays: 7,
|
||||
},
|
||||
})
|
||||
assert.equal(connected.status, 'connected')
|
||||
assert.equal(connected.encryptedPassword, undefined)
|
||||
|
||||
const conversations = await request('/api/conversations?channel=email', { token: session.token })
|
||||
assert.ok(conversations.list.length >= 2)
|
||||
const newerConversation = conversations.list.find((item) => item.emailSubject === newerSubject)
|
||||
const olderConversation = conversations.list.find((item) => item.emailSubject === olderSubject)
|
||||
assert.ok(newerConversation)
|
||||
assert.ok(olderConversation)
|
||||
assert.ok(conversations.list.indexOf(newerConversation) < conversations.list.indexOf(olderConversation))
|
||||
|
||||
const olderMessages = await request(`/api/conversations/${olderConversation.id}/messages`, { token: session.token })
|
||||
await request(`/api/conversations/${olderConversation.id}/read`, {
|
||||
token: session.token,
|
||||
method: 'POST',
|
||||
body: { messageIds: olderMessages.map((message) => message.id) },
|
||||
})
|
||||
const afterRead = await request('/api/conversations?channel=email', { token: session.token })
|
||||
const relevantOrder = afterRead.list
|
||||
.filter((item) => [newerConversation.id, olderConversation.id].includes(item.id))
|
||||
.map((item) => item.id)
|
||||
assert.deepEqual(relevantOrder, [newerConversation.id, olderConversation.id])
|
||||
|
||||
let conversation
|
||||
let incoming
|
||||
for (const candidate of conversations.list.filter((item) => item.customer?.email === 'sender@local.test')) {
|
||||
const candidateMessages = await request(`/api/conversations/${candidate.id}/messages`, { token: session.token })
|
||||
const candidateIncoming = candidateMessages.find((message) => message.sender === 'customer' && message.emailAttachments?.length)
|
||||
if (candidateIncoming) {
|
||||
conversation = candidate
|
||||
incoming = candidateIncoming
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.ok(conversation)
|
||||
assert.ok(incoming)
|
||||
const attachment = await request(
|
||||
`/api/conversations/${conversation.id}/messages/${incoming.id}/attachment?attachmentId=${encodeURIComponent(incoming.emailAttachments[0].id)}`,
|
||||
{ token: session.token },
|
||||
)
|
||||
assert.equal(attachment.name, 'lingxi-proof.txt')
|
||||
assert.ok(attachment.dataUrl.startsWith('data:text/plain;base64,'))
|
||||
|
||||
const replyText = `Lingxi gateway API reply ${suffix}`
|
||||
const sent = await request(`/api/conversations/${conversation.id}/messages`, {
|
||||
token: session.token,
|
||||
method: 'POST',
|
||||
body: { text: replyText, autoTranslate: false },
|
||||
})
|
||||
assert.equal(sent.status, 'sent')
|
||||
assert.ok(sent.emailMessageId)
|
||||
|
||||
const receivedBySender = await waitForSenderMessage((message) => message.messageId === sent.emailMessageId)
|
||||
assert.equal(receivedBySender.text, replyText)
|
||||
assert.ok(receivedBySender.inReplyTo)
|
||||
|
||||
await request(`/api/conversations/${conversation.id}/read`, {
|
||||
token: session.token,
|
||||
method: 'POST',
|
||||
body: { messageIds: [incoming.id] },
|
||||
})
|
||||
const receiver = await fetchEmailMessages(localChannel('receiver@local.test'), { full: true })
|
||||
assert.equal(receiver.messages.find((message) => message.uid === incoming.emailUid)?.seen, true)
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
encryptEmailPassword,
|
||||
fetchEmailMessages,
|
||||
markEmailUidsSeen,
|
||||
sendEmailMessage,
|
||||
verifyEmailConnection,
|
||||
} from '../src/email.js'
|
||||
|
||||
const enabled = process.env.RUN_EMAIL_PROTOCOL_TEST === '1'
|
||||
|
||||
function channel(account) {
|
||||
return {
|
||||
id: `protocol-${account}`,
|
||||
type: 'email',
|
||||
account,
|
||||
displayName: account.startsWith('sender') ? 'Lingxi Sender' : 'Lingxi Receiver',
|
||||
encryptedPassword: encryptEmailPassword('pwd'),
|
||||
imapHost: process.env.EMAIL_TEST_HOST || '127.0.0.1',
|
||||
imapPort: Number(process.env.EMAIL_TEST_IMAP_PORT || 3143),
|
||||
imapSecure: false,
|
||||
imapFolder: 'INBOX',
|
||||
smtpHost: process.env.EMAIL_TEST_HOST || '127.0.0.1',
|
||||
smtpPort: Number(process.env.EMAIL_TEST_SMTP_PORT || 3025),
|
||||
smtpSecure: false,
|
||||
syncDays: 7,
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForMessage(target, predicate) {
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
const result = await fetchEmailMessages(target, { full: true })
|
||||
const message = result.messages.find(predicate)
|
||||
if (message) return { message, result }
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
}
|
||||
throw new Error('等待邮件进入 IMAP INBOX 超时')
|
||||
}
|
||||
|
||||
test('真实 SMTP 发件、IMAP 收件、附件解析、已读与回复头', { skip: !enabled }, async () => {
|
||||
process.env.JWT_SECRET ||= 'lingxi-email-protocol-test-secret'
|
||||
const sender = channel('sender@local.test')
|
||||
const receiver = channel('receiver@local.test')
|
||||
const subject = `Lingxi protocol ${Date.now()}`
|
||||
|
||||
const senderCheck = await verifyEmailConnection(sender, 'pwd')
|
||||
const receiverCheck = await verifyEmailConnection(receiver, 'pwd')
|
||||
assert.ok(senderCheck.latencyMs >= 0)
|
||||
assert.ok(receiverCheck.latencyMs >= 0)
|
||||
assert.ok(!sender.encryptedPassword.includes('pwd'))
|
||||
|
||||
const sent = await sendEmailMessage(sender, {
|
||||
to: receiver.account,
|
||||
subject,
|
||||
text: 'Lingxi IMAP/SMTP real protocol message',
|
||||
attachments: [{
|
||||
filename: 'lingxi-proof.txt',
|
||||
content: Buffer.from('real email attachment'),
|
||||
contentType: 'text/plain',
|
||||
}],
|
||||
})
|
||||
assert.ok(sent.messageId)
|
||||
|
||||
const received = await waitForMessage(receiver, (message) => message.subject === subject)
|
||||
assert.equal(received.message.text, 'Lingxi IMAP/SMTP real protocol message')
|
||||
assert.equal(received.message.from.email, sender.account)
|
||||
assert.equal(received.message.attachments.length, 1)
|
||||
assert.equal(received.message.attachments[0].name, 'lingxi-proof.txt')
|
||||
assert.equal(Buffer.from(received.message.attachments[0].base64, 'base64').toString(), 'real email attachment')
|
||||
|
||||
await markEmailUidsSeen(receiver, [received.message.uid])
|
||||
const marked = await fetchEmailMessages(receiver, { full: true })
|
||||
assert.equal(marked.messages.find((message) => message.uid === received.message.uid)?.seen, true)
|
||||
|
||||
const reply = await sendEmailMessage(receiver, {
|
||||
to: sender.account,
|
||||
subject: `Re: ${subject}`,
|
||||
text: 'Reply received by Lingxi',
|
||||
inReplyTo: received.message.messageId,
|
||||
references: [received.message.messageId],
|
||||
})
|
||||
const replyReceived = await waitForMessage(sender, (message) => message.messageId === reply.messageId)
|
||||
assert.equal(replyReceived.message.inReplyTo, received.message.messageId)
|
||||
assert.ok(replyReceived.message.references.includes(received.message.messageId))
|
||||
})
|
||||
Reference in New Issue
Block a user