Add IMAP email monitoring for scanned card attachments
Watches an email inbox via IMAP IDLE for incoming scanned cards, extracts PDF/image attachments, and feeds them through the existing OCR pipeline. Configurable via the Settings page with test connection support. Made-with: Cursor
This commit is contained in:
parent
3dd4dc16a3
commit
41000337e3
8 changed files with 1078 additions and 0 deletions
473
package-lock.json
generated
473
package-lock.json
generated
|
|
@ -24,7 +24,9 @@
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
"imapflow": "^1.2.18",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
"mailparser": "^3.9.6",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pdf2pic": "^3.2.0",
|
"pdf2pic": "^3.2.0",
|
||||||
|
|
@ -41,6 +43,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/mailparser": "^3.4.6",
|
||||||
"@types/node": "^25.4.0",
|
"@types/node": "^25.4.0",
|
||||||
"@types/pg": "^8.18.0",
|
"@types/pg": "^8.18.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|
@ -2991,6 +2994,12 @@
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pinojs/redact": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@prisma/adapter-pg": {
|
"node_modules/@prisma/adapter-pg": {
|
||||||
"version": "7.4.2",
|
"version": "7.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz",
|
||||||
|
|
@ -3639,6 +3648,19 @@
|
||||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@selderee/plugin-htmlparser2": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sindresorhus/merge-streams": {
|
"node_modules/@sindresorhus/merge-streams": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||||
|
|
@ -4809,6 +4831,30 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mailparser": {
|
||||||
|
"version": "3.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.4.6.tgz",
|
||||||
|
"integrity": "sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"iconv-lite": "^0.6.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mailparser/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.4.0",
|
"version": "25.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz",
|
||||||
|
|
@ -5435,6 +5481,17 @@
|
||||||
"node": ">= 20"
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@zone-eu/mailsplit": {
|
||||||
|
"version": "5.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz",
|
||||||
|
"integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==",
|
||||||
|
"license": "(MIT OR EUPL-1.1+)",
|
||||||
|
"dependencies": {
|
||||||
|
"libbase64": "1.3.0",
|
||||||
|
"libmime": "5.3.7",
|
||||||
|
"libqp": "2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
|
@ -5819,6 +5876,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/atomic-sleep": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
|
|
@ -6799,6 +6865,61 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.3.1",
|
"version": "17.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||||
|
|
@ -6889,6 +7010,15 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/encoding-japanese": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.20.0",
|
"version": "5.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
|
||||||
|
|
@ -6903,6 +7033,18 @@
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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": {
|
"node_modules/env-paths": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
|
|
@ -8443,6 +8585,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/he": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"he": "bin/he"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/headers-polyfill": {
|
"node_modules/headers-polyfill": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
|
||||||
|
|
@ -8475,6 +8626,41 @@
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-to-text": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"htmlparser2": "^8.0.2",
|
||||||
|
"selderee": "^0.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"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.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
|
@ -8548,6 +8734,23 @@
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/imapflow": {
|
||||||
|
"version": "1.2.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.2.18.tgz",
|
||||||
|
"integrity": "sha512-zxYvcG9ckj/UcTRs+ZDT+wJzW8DqkjgWZwc1z4Q28R/4C/1YvJieVETOuR/9ztCXcycURC50PJShMimITvz5wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@zone-eu/mailsplit": "5.4.8",
|
||||||
|
"encoding-japanese": "2.2.0",
|
||||||
|
"iconv-lite": "0.7.2",
|
||||||
|
"libbase64": "1.3.0",
|
||||||
|
"libmime": "5.3.7",
|
||||||
|
"libqp": "2.1.1",
|
||||||
|
"nodemailer": "8.0.4",
|
||||||
|
"pino": "10.3.1",
|
||||||
|
"socks": "2.8.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
|
|
@ -9387,6 +9590,15 @@
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leac": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
|
|
@ -9401,6 +9613,42 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/libbase64": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/libmime": {
|
||||||
|
"version": "5.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz",
|
||||||
|
"integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encoding-japanese": "2.2.0",
|
||||||
|
"iconv-lite": "0.6.3",
|
||||||
|
"libbase64": "1.3.0",
|
||||||
|
"libqp": "2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/libmime/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/libqp": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.31.1",
|
"version": "1.31.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
|
||||||
|
|
@ -9677,6 +9925,15 @@
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
|
|
@ -9808,6 +10065,24 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mailparser": {
|
||||||
|
"version": "3.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.6.tgz",
|
||||||
|
"integrity": "sha512-EJYTDWMrOS1kddK1mTsRkrx2Ngh2nYsg54SRMWVVWGVEGbHH4tod8tqqU9hIRPgGQVboSjFubDn9cboSitbM3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@zone-eu/mailsplit": "5.4.8",
|
||||||
|
"encoding-japanese": "2.2.0",
|
||||||
|
"he": "1.2.0",
|
||||||
|
"html-to-text": "9.0.5",
|
||||||
|
"iconv-lite": "0.7.2",
|
||||||
|
"libmime": "5.3.7",
|
||||||
|
"linkify-it": "5.0.0",
|
||||||
|
"nodemailer": "8.0.4",
|
||||||
|
"punycode.js": "2.3.1",
|
||||||
|
"tlds": "1.261.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -10248,6 +10523,15 @@
|
||||||
"integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
|
"integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "8.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
|
||||||
|
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/npm-run-path": {
|
"node_modules/npm-run-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
||||||
|
|
@ -10435,6 +10719,15 @@
|
||||||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/on-exit-leak-free": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/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": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
|
@ -10648,6 +10941,19 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parseley": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"leac": "^0.6.0",
|
||||||
|
"peberminta": "^0.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|
@ -10717,6 +11023,15 @@
|
||||||
"url": "https://www.paypal.me/yakovmeister"
|
"url": "https://www.paypal.me/yakovmeister"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/peberminta": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/perfect-debounce": {
|
"node_modules/perfect-debounce": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
|
@ -10839,6 +11154,43 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pino": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pkce-challenge": {
|
"node_modules/pkce-challenge": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||||
|
|
@ -11032,6 +11384,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-warning": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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/prompts": {
|
"node_modules/prompts": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||||
|
|
@ -11106,6 +11474,15 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pure-rand": {
|
"node_modules/pure-rand": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||||
|
|
@ -11157,6 +11534,12 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/quick-format-unescaped": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
|
@ -11301,6 +11684,15 @@
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/real-require": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/recast": {
|
"node_modules/recast": {
|
||||||
"version": "0.23.11",
|
"version": "0.23.11",
|
||||||
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz",
|
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz",
|
||||||
|
|
@ -11597,6 +11989,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-stable-stringify": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
|
@ -11609,6 +12010,18 @@
|
||||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/selderee": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"parseley": "^0.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/killymxi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
|
@ -11987,6 +12400,39 @@
|
||||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.0.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.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sonner": {
|
"node_modules/sonner": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||||
|
|
@ -12399,6 +12845,18 @@
|
||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/thread-stream": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"real-require": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tiny-invariant": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
|
@ -12462,6 +12920,15 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tlds": {
|
||||||
|
"version": "1.261.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
|
||||||
|
"integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"tlds": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tldts": {
|
"node_modules/tldts": {
|
||||||
"version": "7.0.25",
|
"version": "7.0.25",
|
||||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz",
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz",
|
||||||
|
|
@ -12735,6 +13202,12 @@
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
"imapflow": "^1.2.18",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
"mailparser": "^3.9.6",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pdf2pic": "^3.2.0",
|
"pdf2pic": "^3.2.0",
|
||||||
|
|
@ -45,6 +47,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/mailparser": "^3.4.6",
|
||||||
"@types/node": "^25.4.0",
|
"@types/node": "^25.4.0",
|
||||||
"@types/pg": "^8.18.0",
|
"@types/pg": "^8.18.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|
|
||||||
|
|
@ -85,4 +85,14 @@ model AppSettings {
|
||||||
|
|
||||||
aiProvider String @default("gateway")
|
aiProvider String @default("gateway")
|
||||||
aiModel String @default("")
|
aiModel String @default("")
|
||||||
|
|
||||||
|
emailImapHost String @default("imap.dreamhost.com")
|
||||||
|
emailImapPort Int @default(993)
|
||||||
|
emailImapUser String @default("echo-ocr@stillwell.cloud")
|
||||||
|
emailImapPass String @default("")
|
||||||
|
emailImapTls Boolean @default(true)
|
||||||
|
emailFolder String @default("INBOX")
|
||||||
|
emailWatching Boolean @default(false)
|
||||||
|
emailProcessed String @default("mark_read")
|
||||||
|
emailProcessedFolder String @default("Processed")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
src/app/api/email-watch/route.ts
Normal file
61
src/app/api/email-watch/route.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
import {
|
||||||
|
startEmailWatching,
|
||||||
|
stopEmailWatching,
|
||||||
|
isEmailWatching,
|
||||||
|
} from "@/lib/email-watcher";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
const action = body.action as string | undefined;
|
||||||
|
|
||||||
|
if (!action || !["start", "stop"].includes(action)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid action. Use 'start' or 'stop'" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "start") {
|
||||||
|
const settings = await prisma.appSettings.findUnique({
|
||||||
|
where: { id: "singleton" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Settings not configured" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.emailImapHost || !settings.emailImapUser || !settings.emailImapPass) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "IMAP host, username, and password are required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await startEmailWatching({
|
||||||
|
host: settings.emailImapHost,
|
||||||
|
port: settings.emailImapPort,
|
||||||
|
user: settings.emailImapUser,
|
||||||
|
pass: settings.emailImapPass,
|
||||||
|
tls: settings.emailImapTls,
|
||||||
|
folder: settings.emailFolder,
|
||||||
|
processedAction: settings.emailProcessed,
|
||||||
|
processedFolder: settings.emailProcessedFolder,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await stopEmailWatching();
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ emailWatching: isEmailWatching() });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[email-watch POST]", error);
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Failed to update email watch";
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/app/api/email-watch/test/route.ts
Normal file
41
src/app/api/email-watch/test/route.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { testEmailConnection } from "@/lib/email-watcher";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
|
||||||
|
const host = String(body.host || "");
|
||||||
|
const port = parseInt(String(body.port)) || 993;
|
||||||
|
const user = String(body.user || "");
|
||||||
|
const pass = String(body.pass || "");
|
||||||
|
const tls = body.tls !== false;
|
||||||
|
|
||||||
|
if (!host || !user || !pass) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Host, username, and password are required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await testEmailConnection({ host, port, user, pass, tls, folder: "INBOX" });
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
folders: result.folders,
|
||||||
|
message: `Connected to ${host} — ${result.folders?.length || 0} folder(s) found`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ ok: false, error: result.error },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[email-watch/test POST]", error);
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Connection test failed";
|
||||||
|
return NextResponse.json({ ok: false, error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,15 @@ export async function PUT(request: NextRequest) {
|
||||||
if (body.aiProvider != null) data.aiProvider = String(body.aiProvider);
|
if (body.aiProvider != null) data.aiProvider = String(body.aiProvider);
|
||||||
if (body.aiModel != null) data.aiModel = String(body.aiModel);
|
if (body.aiModel != null) data.aiModel = String(body.aiModel);
|
||||||
|
|
||||||
|
if (body.emailImapHost != null) data.emailImapHost = String(body.emailImapHost);
|
||||||
|
if (body.emailImapPort != null) data.emailImapPort = Math.max(1, parseInt(String(body.emailImapPort)) || 993);
|
||||||
|
if (body.emailImapUser != null) data.emailImapUser = String(body.emailImapUser);
|
||||||
|
if (body.emailImapPass != null) data.emailImapPass = String(body.emailImapPass);
|
||||||
|
if (body.emailImapTls != null) data.emailImapTls = Boolean(body.emailImapTls);
|
||||||
|
if (body.emailFolder != null) data.emailFolder = String(body.emailFolder);
|
||||||
|
if (body.emailProcessed != null) data.emailProcessed = String(body.emailProcessed);
|
||||||
|
if (body.emailProcessedFolder != null) data.emailProcessedFolder = String(body.emailProcessedFolder);
|
||||||
|
|
||||||
const settings = await prisma.appSettings.upsert({
|
const settings = await prisma.appSettings.upsert({
|
||||||
where: { id: "singleton" },
|
where: { id: "singleton" },
|
||||||
create: {
|
create: {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
Moon,
|
Moon,
|
||||||
Monitor,
|
Monitor,
|
||||||
Bell,
|
Bell,
|
||||||
|
Mail,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
|
|
@ -60,6 +61,15 @@ type SettingsData = {
|
||||||
imageRetentionDays: number;
|
imageRetentionDays: number;
|
||||||
aiProvider: string;
|
aiProvider: string;
|
||||||
aiModel: string;
|
aiModel: string;
|
||||||
|
emailImapHost: string;
|
||||||
|
emailImapPort: number;
|
||||||
|
emailImapUser: string;
|
||||||
|
emailImapPass: string;
|
||||||
|
emailImapTls: boolean;
|
||||||
|
emailFolder: string;
|
||||||
|
emailWatching: boolean;
|
||||||
|
emailProcessed: string;
|
||||||
|
emailProcessedFolder: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NOTIFICATION_STORAGE_KEY = "echo-ocr-notification-prefs";
|
const NOTIFICATION_STORAGE_KEY = "echo-ocr-notification-prefs";
|
||||||
|
|
@ -99,12 +109,22 @@ export default function SettingsPage() {
|
||||||
imageRetentionDays: 180,
|
imageRetentionDays: 180,
|
||||||
aiProvider: "gateway",
|
aiProvider: "gateway",
|
||||||
aiModel: "",
|
aiModel: "",
|
||||||
|
emailImapHost: "imap.dreamhost.com",
|
||||||
|
emailImapPort: 993,
|
||||||
|
emailImapUser: "echo-ocr@stillwell.cloud",
|
||||||
|
emailImapPass: "",
|
||||||
|
emailImapTls: true,
|
||||||
|
emailFolder: "INBOX",
|
||||||
|
emailWatching: false,
|
||||||
|
emailProcessed: "mark_read",
|
||||||
|
emailProcessedFolder: "Processed",
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [saving, setSaving] = React.useState(false);
|
const [saving, setSaving] = React.useState(false);
|
||||||
const [aiTestStatus, setAiTestStatus] = React.useState<"idle" | "testing" | "success" | "error">("idle");
|
const [aiTestStatus, setAiTestStatus] = React.useState<"idle" | "testing" | "success" | "error">("idle");
|
||||||
const [cleanupStatus, setCleanupStatus] = React.useState<{ sourcesEligible: number; imagesEligible: number } | null>(null);
|
const [cleanupStatus, setCleanupStatus] = React.useState<{ sourcesEligible: number; imagesEligible: number } | null>(null);
|
||||||
const [cleaning, setCleaning] = React.useState(false);
|
const [cleaning, setCleaning] = React.useState(false);
|
||||||
|
const [emailTestStatus, setEmailTestStatus] = React.useState<"idle" | "testing" | "success" | "error">("idle");
|
||||||
const [notifPrefs, setNotifPrefs] = React.useState<NotificationPrefs>(DEFAULT_NOTIFICATION_PREFS);
|
const [notifPrefs, setNotifPrefs] = React.useState<NotificationPrefs>(DEFAULT_NOTIFICATION_PREFS);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -213,6 +233,54 @@ export default function SettingsPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleEmailWatch = async () => {
|
||||||
|
try {
|
||||||
|
const action = settings.emailWatching ? "stop" : "start";
|
||||||
|
const res = await fetch("/api/email-watch", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data.error || "Failed");
|
||||||
|
}
|
||||||
|
setSettings((s) => ({ ...s, emailWatching: !s.emailWatching }));
|
||||||
|
toast.success(action === "start" ? "Email monitoring started" : "Email monitoring stopped");
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : "Failed to toggle email monitoring");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testEmailConnection = async () => {
|
||||||
|
setEmailTestStatus("testing");
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/email-watch/test", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
host: settings.emailImapHost,
|
||||||
|
port: settings.emailImapPort,
|
||||||
|
user: settings.emailImapUser,
|
||||||
|
pass: settings.emailImapPass,
|
||||||
|
tls: settings.emailImapTls,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (res.ok && data.ok) {
|
||||||
|
setEmailTestStatus("success");
|
||||||
|
toast.success(data.message || "Connected successfully");
|
||||||
|
} else {
|
||||||
|
setEmailTestStatus("error");
|
||||||
|
toast.error(data.error || "Connection failed");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setEmailTestStatus("error");
|
||||||
|
toast.error("Cannot reach email server. Check your configuration.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const runCleanup = async () => {
|
const runCleanup = async () => {
|
||||||
setCleaning(true);
|
setCleaning(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -471,6 +539,154 @@ export default function SettingsPage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card variant="glass">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Mail className="size-4 text-primary" />
|
||||||
|
Email Monitoring
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Watch an inbox for scanned card attachments</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className={
|
||||||
|
emailTestStatus === "success"
|
||||||
|
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
|
||||||
|
: emailTestStatus === "error"
|
||||||
|
? "bg-red-500/10 text-red-700 dark:text-red-300"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{emailTestStatus === "success" && <Wifi className="mr-1 size-3" />}
|
||||||
|
{emailTestStatus === "error" && <WifiOff className="mr-1 size-3" />}
|
||||||
|
{emailTestStatus === "testing" && <Loader2 className="mr-1 size-3 animate-spin" />}
|
||||||
|
{emailTestStatus === "success"
|
||||||
|
? "Connected"
|
||||||
|
: emailTestStatus === "error"
|
||||||
|
? "Error"
|
||||||
|
: emailTestStatus === "testing"
|
||||||
|
? "Testing..."
|
||||||
|
: "Not tested"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">IMAP Host</Label>
|
||||||
|
<Input
|
||||||
|
value={settings.emailImapHost}
|
||||||
|
onChange={(e) => setSettings((s) => ({ ...s, emailImapHost: e.target.value }))}
|
||||||
|
placeholder="imap.dreamhost.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">Port</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={settings.emailImapPort}
|
||||||
|
onChange={(e) => setSettings((s) => ({ ...s, emailImapPort: parseInt(e.target.value) || 993 }))}
|
||||||
|
placeholder="993"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">Username</Label>
|
||||||
|
<Input
|
||||||
|
value={settings.emailImapUser}
|
||||||
|
onChange={(e) => setSettings((s) => ({ ...s, emailImapUser: e.target.value }))}
|
||||||
|
placeholder="echo-ocr@stillwell.cloud"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">Password</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={settings.emailImapPass}
|
||||||
|
onChange={(e) => setSettings((s) => ({ ...s, emailImapPass: e.target.value }))}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">Folder</Label>
|
||||||
|
<Input
|
||||||
|
value={settings.emailFolder}
|
||||||
|
onChange={(e) => setSettings((s) => ({ ...s, emailFolder: e.target.value }))}
|
||||||
|
placeholder="INBOX"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">After Processing</Label>
|
||||||
|
<Select
|
||||||
|
value={settings.emailProcessed}
|
||||||
|
onValueChange={(val) => val && setSettings((s) => ({ ...s, emailProcessed: val }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="mark_read">Mark as Read</SelectItem>
|
||||||
|
<SelectItem value="move">Move to Folder</SelectItem>
|
||||||
|
<SelectItem value="delete">Delete</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{settings.emailProcessed === "move" && (
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">Move to Folder</Label>
|
||||||
|
<Input
|
||||||
|
value={settings.emailProcessedFolder}
|
||||||
|
onChange={(e) => setSettings((s) => ({ ...s, emailProcessedFolder: e.target.value }))}
|
||||||
|
placeholder="Processed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Label className="text-xs font-medium text-muted-foreground">TLS / SSL</Label>
|
||||||
|
<Switch
|
||||||
|
checked={settings.emailImapTls}
|
||||||
|
onCheckedChange={(val: boolean) => setSettings((s) => ({ ...s, emailImapTls: val }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-xl"
|
||||||
|
onClick={testEmailConnection}
|
||||||
|
disabled={emailTestStatus === "testing" || !settings.emailImapHost || !settings.emailImapUser || !settings.emailImapPass}
|
||||||
|
>
|
||||||
|
{emailTestStatus === "testing" ? (
|
||||||
|
<Loader2 className="mr-2 size-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Wifi className="mr-2 size-3" />
|
||||||
|
)}
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={settings.emailWatching ? "destructive" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="rounded-xl"
|
||||||
|
onClick={toggleEmailWatch}
|
||||||
|
disabled={!settings.emailImapHost || !settings.emailImapUser || !settings.emailImapPass}
|
||||||
|
>
|
||||||
|
{settings.emailWatching ? "Stop Monitoring" : "Start Monitoring"}
|
||||||
|
</Button>
|
||||||
|
{settings.emailWatching && (
|
||||||
|
<Badge className="bg-emerald-500/10 text-emerald-700 dark:text-emerald-300">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card variant="glass">
|
<Card variant="glass">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
|
|
||||||
265
src/lib/email-watcher.ts
Normal file
265
src/lib/email-watcher.ts
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
import { ImapFlow } from "imapflow";
|
||||||
|
import { simpleParser } from "mailparser";
|
||||||
|
import { prisma } from "./db";
|
||||||
|
import { processFile } from "./ocr";
|
||||||
|
|
||||||
|
const ALLOWED_CONTENT_TYPES = [
|
||||||
|
"application/pdf",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
];
|
||||||
|
|
||||||
|
type EmailConfig = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
pass: string;
|
||||||
|
tls: boolean;
|
||||||
|
folder: string;
|
||||||
|
processedAction: string;
|
||||||
|
processedFolder: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let client: ImapFlow | null = null;
|
||||||
|
let watching = false;
|
||||||
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let currentConfig: EmailConfig | null = null;
|
||||||
|
|
||||||
|
function log(msg: string, ...args: unknown[]) {
|
||||||
|
console.log(`[email-watcher] ${msg}`, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logError(msg: string, ...args: unknown[]) {
|
||||||
|
console.error(`[email-watcher] ${msg}`, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMessage(client: ImapFlow, uid: number, config: EmailConfig) {
|
||||||
|
const result = await client.fetchOne(String(uid), { source: true }, { uid: true });
|
||||||
|
if (!result) return;
|
||||||
|
const rawSource = (result as unknown as { source?: Buffer }).source;
|
||||||
|
if (!rawSource) return;
|
||||||
|
|
||||||
|
const parsed = await simpleParser(rawSource);
|
||||||
|
const subject = parsed.subject || "(no subject)";
|
||||||
|
const from = parsed.from?.text || "unknown";
|
||||||
|
|
||||||
|
const attachments = (parsed.attachments || []).filter((att) => {
|
||||||
|
const ct = att.contentType?.toLowerCase() || "";
|
||||||
|
return ALLOWED_CONTENT_TYPES.includes(ct);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (attachments.length === 0) {
|
||||||
|
log(`Skipping email from ${from} "${subject}" — no valid attachments`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Processing email from ${from} "${subject}" — ${attachments.length} attachment(s)`);
|
||||||
|
|
||||||
|
for (const att of attachments) {
|
||||||
|
const fileName = att.filename || `email-attachment-${Date.now()}`;
|
||||||
|
const buffer = att.content;
|
||||||
|
const isPdf = att.contentType?.toLowerCase() === "application/pdf";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const job = await prisma.processingJob.create({
|
||||||
|
data: {
|
||||||
|
fileName,
|
||||||
|
filePath: `email/${fileName}`,
|
||||||
|
status: "queued",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
processFile(job.id, fileName, buffer, isPdf).catch((err) => {
|
||||||
|
logError(`Processing failed for ${fileName}:`, err);
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`Queued: ${fileName} (job ${job.id})`);
|
||||||
|
} catch (err) {
|
||||||
|
logError(`Failed to create job for ${fileName}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.processedAction === "mark_read") {
|
||||||
|
await client.messageFlagsAdd(String(uid), ["\\Seen"], { uid: true });
|
||||||
|
} else if (config.processedAction === "move") {
|
||||||
|
try {
|
||||||
|
await client.messageMove(String(uid), config.processedFolder, { uid: true });
|
||||||
|
} catch {
|
||||||
|
log(`Could not move to "${config.processedFolder}", marking as read instead`);
|
||||||
|
await client.messageFlagsAdd(String(uid), ["\\Seen"], { uid: true });
|
||||||
|
}
|
||||||
|
} else if (config.processedAction === "delete") {
|
||||||
|
await client.messageDelete(String(uid), { uid: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollLoop(config: EmailConfig) {
|
||||||
|
if (!client || !watching) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lock = await client.getMailboxLock(config.folder);
|
||||||
|
try {
|
||||||
|
const searchResult = await client.search({ seen: false }, { uid: true });
|
||||||
|
const unseen = Array.isArray(searchResult) ? searchResult : [];
|
||||||
|
if (unseen.length > 0) {
|
||||||
|
log(`Found ${unseen.length} unseen message(s)`);
|
||||||
|
for (const uid of unseen) {
|
||||||
|
if (!watching) break;
|
||||||
|
try {
|
||||||
|
await handleMessage(client, uid, config);
|
||||||
|
} catch (err) {
|
||||||
|
logError(`Error handling message UID ${uid}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.release();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logError("Error during poll:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!watching || !client) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.idle();
|
||||||
|
} catch {
|
||||||
|
// IDLE interrupted or connection lost — reconnect handles it
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watching) {
|
||||||
|
setImmediate(() => pollLoop(config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect(config: EmailConfig) {
|
||||||
|
if (client) {
|
||||||
|
try { await client.logout(); } catch {}
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
client = new ImapFlow({
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
secure: config.tls,
|
||||||
|
auth: { user: config.user, pass: config.pass },
|
||||||
|
logger: false,
|
||||||
|
emitLogs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (err: Error) => {
|
||||||
|
logError("Connection error:", err.message);
|
||||||
|
if (watching) scheduleReconnect(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("close", () => {
|
||||||
|
log("Connection closed");
|
||||||
|
if (watching) scheduleReconnect(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
log(`Connected to ${config.host}:${config.port} as ${config.user}`);
|
||||||
|
|
||||||
|
pollLoop(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect(config: EmailConfig) {
|
||||||
|
if (reconnectTimer) return;
|
||||||
|
const delay = 10_000;
|
||||||
|
log(`Reconnecting in ${delay / 1000}s...`);
|
||||||
|
reconnectTimer = setTimeout(async () => {
|
||||||
|
reconnectTimer = null;
|
||||||
|
if (!watching) return;
|
||||||
|
try {
|
||||||
|
await connect(config);
|
||||||
|
} catch (err) {
|
||||||
|
logError("Reconnect failed:", err);
|
||||||
|
scheduleReconnect(config);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startEmailWatching(config: EmailConfig): Promise<void> {
|
||||||
|
if (watching) await stopEmailWatching();
|
||||||
|
|
||||||
|
watching = true;
|
||||||
|
currentConfig = config;
|
||||||
|
|
||||||
|
await prisma.appSettings.upsert({
|
||||||
|
where: { id: "singleton" },
|
||||||
|
update: { emailWatching: true },
|
||||||
|
create: { id: "singleton", emailWatching: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await connect(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopEmailWatching(): Promise<void> {
|
||||||
|
watching = false;
|
||||||
|
currentConfig = null;
|
||||||
|
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
try { await client.logout(); } catch {}
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.appSettings.upsert({
|
||||||
|
where: { id: "singleton" },
|
||||||
|
update: { emailWatching: false },
|
||||||
|
create: { id: "singleton", emailWatching: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
log("Stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmailWatching(): boolean {
|
||||||
|
return watching;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEmailWatchStatus() {
|
||||||
|
return {
|
||||||
|
watching,
|
||||||
|
connected: client !== null && client.usable !== false,
|
||||||
|
config: currentConfig
|
||||||
|
? { host: currentConfig.host, user: currentConfig.user, folder: currentConfig.folder }
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testEmailConnection(config: Omit<EmailConfig, "processedAction" | "processedFolder">): Promise<{ ok: boolean; folders?: string[]; error?: string }> {
|
||||||
|
let testClient: ImapFlow | null = null;
|
||||||
|
try {
|
||||||
|
testClient = new ImapFlow({
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
secure: config.tls,
|
||||||
|
auth: { user: config.user, pass: config.pass },
|
||||||
|
logger: false,
|
||||||
|
emitLogs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testClient.connect();
|
||||||
|
const folders: string[] = [];
|
||||||
|
const list = await testClient.list();
|
||||||
|
for (const folder of list) {
|
||||||
|
folders.push(folder.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
await testClient.logout();
|
||||||
|
return { ok: true, folders };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : "Connection failed";
|
||||||
|
return { ok: false, error: message };
|
||||||
|
} finally {
|
||||||
|
if (testClient) {
|
||||||
|
try { await testClient.logout(); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue