├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── config.ts ├── dns-creation.ts ├── dns-verification.ts ├── index.ts ├── network.ts └── smtp-server.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /cwd 3 | /dist 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development & Test files 2 | node_modules/ 3 | src/ 4 | .eslintrc 5 | .prettierrc 6 | tsconfig.json 7 | eslint.config.js 8 | *.tsbuildinfo 9 | *.log 10 | .DS_Store 11 | 12 | # Runtime files 13 | /cwd 14 | 15 | # CI/CD & Version Control 16 | .github/ 17 | .git/ 18 | .gitignore 19 | 20 | # Documentation & examples not needed in published package 21 | docs/ 22 | examples/ 23 | *.md 24 | !README.md 25 | !LICENSE.md 26 | 27 | # Test files 28 | test/ 29 | __tests__/ 30 | *.test.ts 31 | *.spec.ts 32 | 33 | # Development configs 34 | .vscode/ 35 | .idea/ 36 | .editorconfig 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "arrowParens": "avoid", 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 9 | }, 10 | "eslint.validate": [ 11 | "typescript" 12 | ], 13 | "files.eol": "\n" 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TinkSES 2 | 3 | [![npm version](https://img.shields.io/npm/v/tinkses.svg)](https://www.npmjs.com/package/tinkses) 4 | 5 | An open-source mail sending service, you can use it to send emails from your own server. 6 | 7 | Aka, a self-hosted SES. 8 | 9 | Oh, we don't have any other services dependencies, such as AWS SES, SendGrid, Mailgun, or Database, Redis, etc. 10 | 11 | > In case you have any interest, you can also look at our email service [TinkMail](https://tinkmail.me) for both personal and business uses. 12 | 13 | ## Features 14 | 15 | - Send emails using SMTP 16 | - Password authentication 17 | - Protect your domain and IP address with DKIM and SPF 18 | - DKIM signing 19 | - SPF records 20 | - DMARC records 21 | - Easy to use 22 | - Attachments 23 | - HTML emails 24 | 25 | ## Usage 26 | 27 | ### Checking SMTP Connectivity 28 | 29 | Before you start, you should have a working server to send emails. And please remember to check your server's connectivity to popular email providers. 30 | 31 | Please note, major server providers may block port 25 for SMTP, so you have to find a good server provider. 32 | 33 | You can verify if your server can connect to popular email providers: 34 | 35 | ```sh 36 | npx tinkses check-smtp 37 | ``` 38 | 39 | ### Getting Started 40 | 41 | First-time setup with the interactive init command: 42 | 43 | ```sh 44 | npx tinkses init 45 | ``` 46 | 47 | This will: 48 | - Detect your network interfaces (IPv4 and IPv6) 49 | - Generate DKIM keys 50 | - Help you set up a configuration file 51 | - Generate DNS records (SPF, DKIM, DMARC) for your domain 52 | 53 | ### Starting the Server 54 | 55 | After initialization, start the SMTP server: 56 | 57 | ```sh 58 | npx tinkses 59 | ``` 60 | 61 | ### Configuration 62 | 63 | If no configuration file is found, an interactive setup will run automatically. You can also specify a configuration file using the `-c` or `--config` option. 64 | 65 | ```sh 66 | npx tinkses -c /path/to/config.json 67 | ``` 68 | 69 | ### Configuration File 70 | 71 | The configuration file is a JSON file that contains the following fields: 72 | 73 | ```json 74 | { 75 | "port": 25, 76 | "host": "localhost", 77 | "username": "user", 78 | "password": "password", 79 | "domain": "example.com", 80 | "ip": ["192.0.2.1", "2001:db8::1"], 81 | "dkim": { 82 | "privateKey": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", 83 | "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n", 84 | "selector": "default" 85 | } 86 | } 87 | ``` 88 | 89 | - `port`: The port to listen on. Default is `25`. 90 | - `host`: The host to listen on. Default is `localhost`. 91 | - `username`: The username to authenticate with. Default is `user`. 92 | - `password`: The password to authenticate with. Default is `password`. 93 | - `domain`: The domain to use for sending emails. Default is `example.com`. 94 | - `ip`: An array of IP addresses used for sending emails. Used in SPF record generation. 95 | - `dkim`: The DKIM configuration with the following fields: 96 | - `privateKey`: The private key content (or file path) used for DKIM signing. 97 | - `publicKey`: The public key content used in your DNS records. 98 | - `selector`: The selector to use for DKIM signing. Default is `default`. 99 | 100 | ## DNS Configuration 101 | 102 | For maximum deliverability, configure your DNS with: 103 | 104 | 1. **SPF Record** - Specifies which IP addresses can send email for your domain 105 | 2. **DKIM Record** - Adds a digital signature to verify email authenticity 106 | 3. **DMARC Record** - Policy for handling emails that fail SPF/DKIM checks 107 | 108 | TinkSES generates these records during initialization and provides guidance on adding them to your DNS configuration. 109 | 110 | ## License 111 | 112 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 113 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import tseslint from '@typescript-eslint/eslint-plugin'; 3 | import tsparser from '@typescript-eslint/parser'; 4 | import eslintPluginPrettier from 'eslint-plugin-prettier'; 5 | import globals from 'globals'; 6 | 7 | export default [ 8 | js.configs.recommended, 9 | { 10 | ignores: ['dist/**', 'node_modules/**'] 11 | }, 12 | { 13 | files: ['**/*.ts'], 14 | plugins: { 15 | '@typescript-eslint': tseslint, 16 | 'prettier': eslintPluginPrettier 17 | }, 18 | languageOptions: { 19 | parser: tsparser, 20 | parserOptions: { 21 | ecmaVersion: 'latest', 22 | sourceType: 'module', 23 | }, 24 | globals: globals.node, 25 | }, 26 | rules: { 27 | ...tseslint.configs.recommended.rules, 28 | 'prettier/prettier': 'error', 29 | '@typescript-eslint/explicit-module-boundary-types': 'off', 30 | 'no-unused-vars': 'off', 31 | '@typescript-eslint/no-unused-vars': ['off'], 32 | 'no-console': 'off', 33 | '@typescript-eslint/no-explicit-any': 'warn' 34 | } 35 | } 36 | ]; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinkses", 3 | "version": "0.1.3", 4 | "description": "An open-source mail sending service, you can use it to send emails from your own server.", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "tinkses": "dist/index.js" 9 | }, 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.js", 13 | "types": "./dist/index.d.ts" 14 | }, 15 | "./server": { 16 | "import": "./dist/smtp-server.js", 17 | "types": "./dist/smtp-server.d.ts" 18 | }, 19 | "./config": { 20 | "import": "./dist/config.js", 21 | "types": "./dist/config.d.ts" 22 | } 23 | }, 24 | "scripts": { 25 | "clean": "tsc --build --clean", 26 | "prebuild": "npm run clean", 27 | "build": "tsc", 28 | "start": "node dist/index.js", 29 | "dev": "tsx src/index.ts", 30 | "watch": "tsc --watch", 31 | "test": "echo \"Error: no test specified\" && exit 1", 32 | "lint": "eslint . --ext .ts", 33 | "lint:fix": "eslint . --ext .ts --fix", 34 | "format": "prettier --write \"src/**/*.ts\"", 35 | "prepublishOnly": "npm run build" 36 | }, 37 | "keywords": [ 38 | "email", 39 | "smtp", 40 | "ses", 41 | "dkim", 42 | "spf" 43 | ], 44 | "author": "", 45 | "license": "MIT", 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/tinkink/tinkses.git" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/tinkink/tinkses/issues" 52 | }, 53 | "homepage": "https://github.com/tinkink/tinkses", 54 | "files": [ 55 | "dist", 56 | "README.md", 57 | "LICENSE" 58 | ], 59 | "engines": { 60 | "node": ">=18.0.0" 61 | }, 62 | "dependencies": { 63 | "commander": "^11.1.0", 64 | "dkim": "^0.8.0", 65 | "inquirer": "^12.6.0", 66 | "mailparser": "^3.6.5", 67 | "nodemailer": "^6.9.7", 68 | "smtp-server": "^3.13.0" 69 | }, 70 | "devDependencies": { 71 | "@eslint/js": "^9.25.1", 72 | "@types/mailparser": "^3.4.4", 73 | "@types/node": "^20.10.0", 74 | "@types/nodemailer": "^6.4.14", 75 | "@types/smtp-server": "^3.5.10", 76 | "@typescript-eslint/eslint-plugin": "^8.31.0", 77 | "@typescript-eslint/parser": "^8.31.0", 78 | "eslint": "^9.25.1", 79 | "eslint-config-prettier": "^10.1.2", 80 | "eslint-plugin-prettier": "^5.2.6", 81 | "globals": "^16.0.0", 82 | "prettier": "^3.5.3", 83 | "tsx": "^4.19.3", 84 | "typescript": "^5.3.2" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | commander: 12 | specifier: ^11.1.0 13 | version: 11.1.0 14 | dkim: 15 | specifier: ^0.8.0 16 | version: 0.8.0 17 | inquirer: 18 | specifier: ^12.6.0 19 | version: 12.6.0(@types/node@20.17.31) 20 | mailparser: 21 | specifier: ^3.6.5 22 | version: 3.7.2 23 | nodemailer: 24 | specifier: ^6.9.7 25 | version: 6.10.1 26 | smtp-server: 27 | specifier: ^3.13.0 28 | version: 3.13.6 29 | devDependencies: 30 | '@eslint/js': 31 | specifier: ^9.25.1 32 | version: 9.25.1 33 | '@types/mailparser': 34 | specifier: ^3.4.4 35 | version: 3.4.5 36 | '@types/node': 37 | specifier: ^20.10.0 38 | version: 20.17.31 39 | '@types/nodemailer': 40 | specifier: ^6.4.14 41 | version: 6.4.17 42 | '@types/smtp-server': 43 | specifier: ^3.5.10 44 | version: 3.5.10 45 | '@typescript-eslint/eslint-plugin': 46 | specifier: ^8.31.0 47 | version: 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) 48 | '@typescript-eslint/parser': 49 | specifier: ^8.31.0 50 | version: 8.31.0(eslint@9.25.1)(typescript@5.8.3) 51 | eslint: 52 | specifier: ^9.25.1 53 | version: 9.25.1 54 | eslint-config-prettier: 55 | specifier: ^10.1.2 56 | version: 10.1.2(eslint@9.25.1) 57 | eslint-plugin-prettier: 58 | specifier: ^5.2.6 59 | version: 5.2.6(eslint-config-prettier@10.1.2(eslint@9.25.1))(eslint@9.25.1)(prettier@3.5.3) 60 | globals: 61 | specifier: ^16.0.0 62 | version: 16.0.0 63 | prettier: 64 | specifier: ^3.5.3 65 | version: 3.5.3 66 | tsx: 67 | specifier: ^4.19.3 68 | version: 4.19.3 69 | typescript: 70 | specifier: ^5.3.2 71 | version: 5.8.3 72 | 73 | packages: 74 | 75 | '@esbuild/aix-ppc64@0.25.3': 76 | resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} 77 | engines: {node: '>=18'} 78 | cpu: [ppc64] 79 | os: [aix] 80 | 81 | '@esbuild/android-arm64@0.25.3': 82 | resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} 83 | engines: {node: '>=18'} 84 | cpu: [arm64] 85 | os: [android] 86 | 87 | '@esbuild/android-arm@0.25.3': 88 | resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} 89 | engines: {node: '>=18'} 90 | cpu: [arm] 91 | os: [android] 92 | 93 | '@esbuild/android-x64@0.25.3': 94 | resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} 95 | engines: {node: '>=18'} 96 | cpu: [x64] 97 | os: [android] 98 | 99 | '@esbuild/darwin-arm64@0.25.3': 100 | resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} 101 | engines: {node: '>=18'} 102 | cpu: [arm64] 103 | os: [darwin] 104 | 105 | '@esbuild/darwin-x64@0.25.3': 106 | resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} 107 | engines: {node: '>=18'} 108 | cpu: [x64] 109 | os: [darwin] 110 | 111 | '@esbuild/freebsd-arm64@0.25.3': 112 | resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} 113 | engines: {node: '>=18'} 114 | cpu: [arm64] 115 | os: [freebsd] 116 | 117 | '@esbuild/freebsd-x64@0.25.3': 118 | resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} 119 | engines: {node: '>=18'} 120 | cpu: [x64] 121 | os: [freebsd] 122 | 123 | '@esbuild/linux-arm64@0.25.3': 124 | resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} 125 | engines: {node: '>=18'} 126 | cpu: [arm64] 127 | os: [linux] 128 | 129 | '@esbuild/linux-arm@0.25.3': 130 | resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} 131 | engines: {node: '>=18'} 132 | cpu: [arm] 133 | os: [linux] 134 | 135 | '@esbuild/linux-ia32@0.25.3': 136 | resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} 137 | engines: {node: '>=18'} 138 | cpu: [ia32] 139 | os: [linux] 140 | 141 | '@esbuild/linux-loong64@0.25.3': 142 | resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} 143 | engines: {node: '>=18'} 144 | cpu: [loong64] 145 | os: [linux] 146 | 147 | '@esbuild/linux-mips64el@0.25.3': 148 | resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} 149 | engines: {node: '>=18'} 150 | cpu: [mips64el] 151 | os: [linux] 152 | 153 | '@esbuild/linux-ppc64@0.25.3': 154 | resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} 155 | engines: {node: '>=18'} 156 | cpu: [ppc64] 157 | os: [linux] 158 | 159 | '@esbuild/linux-riscv64@0.25.3': 160 | resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} 161 | engines: {node: '>=18'} 162 | cpu: [riscv64] 163 | os: [linux] 164 | 165 | '@esbuild/linux-s390x@0.25.3': 166 | resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} 167 | engines: {node: '>=18'} 168 | cpu: [s390x] 169 | os: [linux] 170 | 171 | '@esbuild/linux-x64@0.25.3': 172 | resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} 173 | engines: {node: '>=18'} 174 | cpu: [x64] 175 | os: [linux] 176 | 177 | '@esbuild/netbsd-arm64@0.25.3': 178 | resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} 179 | engines: {node: '>=18'} 180 | cpu: [arm64] 181 | os: [netbsd] 182 | 183 | '@esbuild/netbsd-x64@0.25.3': 184 | resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} 185 | engines: {node: '>=18'} 186 | cpu: [x64] 187 | os: [netbsd] 188 | 189 | '@esbuild/openbsd-arm64@0.25.3': 190 | resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} 191 | engines: {node: '>=18'} 192 | cpu: [arm64] 193 | os: [openbsd] 194 | 195 | '@esbuild/openbsd-x64@0.25.3': 196 | resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} 197 | engines: {node: '>=18'} 198 | cpu: [x64] 199 | os: [openbsd] 200 | 201 | '@esbuild/sunos-x64@0.25.3': 202 | resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} 203 | engines: {node: '>=18'} 204 | cpu: [x64] 205 | os: [sunos] 206 | 207 | '@esbuild/win32-arm64@0.25.3': 208 | resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} 209 | engines: {node: '>=18'} 210 | cpu: [arm64] 211 | os: [win32] 212 | 213 | '@esbuild/win32-ia32@0.25.3': 214 | resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} 215 | engines: {node: '>=18'} 216 | cpu: [ia32] 217 | os: [win32] 218 | 219 | '@esbuild/win32-x64@0.25.3': 220 | resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} 221 | engines: {node: '>=18'} 222 | cpu: [x64] 223 | os: [win32] 224 | 225 | '@eslint-community/eslint-utils@4.6.1': 226 | resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} 227 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 228 | peerDependencies: 229 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 230 | 231 | '@eslint-community/regexpp@4.12.1': 232 | resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} 233 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 234 | 235 | '@eslint/config-array@0.20.0': 236 | resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} 237 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 238 | 239 | '@eslint/config-helpers@0.2.1': 240 | resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} 241 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 242 | 243 | '@eslint/core@0.13.0': 244 | resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} 245 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 246 | 247 | '@eslint/eslintrc@3.3.1': 248 | resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} 249 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 250 | 251 | '@eslint/js@9.25.1': 252 | resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==} 253 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 254 | 255 | '@eslint/object-schema@2.1.6': 256 | resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} 257 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 258 | 259 | '@eslint/plugin-kit@0.2.8': 260 | resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} 261 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 262 | 263 | '@humanfs/core@0.19.1': 264 | resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} 265 | engines: {node: '>=18.18.0'} 266 | 267 | '@humanfs/node@0.16.6': 268 | resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} 269 | engines: {node: '>=18.18.0'} 270 | 271 | '@humanwhocodes/module-importer@1.0.1': 272 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 273 | engines: {node: '>=12.22'} 274 | 275 | '@humanwhocodes/retry@0.3.1': 276 | resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} 277 | engines: {node: '>=18.18'} 278 | 279 | '@humanwhocodes/retry@0.4.2': 280 | resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} 281 | engines: {node: '>=18.18'} 282 | 283 | '@inquirer/checkbox@4.1.5': 284 | resolution: {integrity: sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==} 285 | engines: {node: '>=18'} 286 | peerDependencies: 287 | '@types/node': '>=18' 288 | peerDependenciesMeta: 289 | '@types/node': 290 | optional: true 291 | 292 | '@inquirer/confirm@5.1.9': 293 | resolution: {integrity: sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==} 294 | engines: {node: '>=18'} 295 | peerDependencies: 296 | '@types/node': '>=18' 297 | peerDependenciesMeta: 298 | '@types/node': 299 | optional: true 300 | 301 | '@inquirer/core@10.1.10': 302 | resolution: {integrity: sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==} 303 | engines: {node: '>=18'} 304 | peerDependencies: 305 | '@types/node': '>=18' 306 | peerDependenciesMeta: 307 | '@types/node': 308 | optional: true 309 | 310 | '@inquirer/editor@4.2.10': 311 | resolution: {integrity: sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==} 312 | engines: {node: '>=18'} 313 | peerDependencies: 314 | '@types/node': '>=18' 315 | peerDependenciesMeta: 316 | '@types/node': 317 | optional: true 318 | 319 | '@inquirer/expand@4.0.12': 320 | resolution: {integrity: sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==} 321 | engines: {node: '>=18'} 322 | peerDependencies: 323 | '@types/node': '>=18' 324 | peerDependenciesMeta: 325 | '@types/node': 326 | optional: true 327 | 328 | '@inquirer/figures@1.0.11': 329 | resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} 330 | engines: {node: '>=18'} 331 | 332 | '@inquirer/input@4.1.9': 333 | resolution: {integrity: sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==} 334 | engines: {node: '>=18'} 335 | peerDependencies: 336 | '@types/node': '>=18' 337 | peerDependenciesMeta: 338 | '@types/node': 339 | optional: true 340 | 341 | '@inquirer/number@3.0.12': 342 | resolution: {integrity: sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==} 343 | engines: {node: '>=18'} 344 | peerDependencies: 345 | '@types/node': '>=18' 346 | peerDependenciesMeta: 347 | '@types/node': 348 | optional: true 349 | 350 | '@inquirer/password@4.0.12': 351 | resolution: {integrity: sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==} 352 | engines: {node: '>=18'} 353 | peerDependencies: 354 | '@types/node': '>=18' 355 | peerDependenciesMeta: 356 | '@types/node': 357 | optional: true 358 | 359 | '@inquirer/prompts@7.5.0': 360 | resolution: {integrity: sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==} 361 | engines: {node: '>=18'} 362 | peerDependencies: 363 | '@types/node': '>=18' 364 | peerDependenciesMeta: 365 | '@types/node': 366 | optional: true 367 | 368 | '@inquirer/rawlist@4.1.0': 369 | resolution: {integrity: sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==} 370 | engines: {node: '>=18'} 371 | peerDependencies: 372 | '@types/node': '>=18' 373 | peerDependenciesMeta: 374 | '@types/node': 375 | optional: true 376 | 377 | '@inquirer/search@3.0.12': 378 | resolution: {integrity: sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==} 379 | engines: {node: '>=18'} 380 | peerDependencies: 381 | '@types/node': '>=18' 382 | peerDependenciesMeta: 383 | '@types/node': 384 | optional: true 385 | 386 | '@inquirer/select@4.2.0': 387 | resolution: {integrity: sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==} 388 | engines: {node: '>=18'} 389 | peerDependencies: 390 | '@types/node': '>=18' 391 | peerDependenciesMeta: 392 | '@types/node': 393 | optional: true 394 | 395 | '@inquirer/type@3.0.6': 396 | resolution: {integrity: sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==} 397 | engines: {node: '>=18'} 398 | peerDependencies: 399 | '@types/node': '>=18' 400 | peerDependenciesMeta: 401 | '@types/node': 402 | optional: true 403 | 404 | '@nodelib/fs.scandir@2.1.5': 405 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 406 | engines: {node: '>= 8'} 407 | 408 | '@nodelib/fs.stat@2.0.5': 409 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 410 | engines: {node: '>= 8'} 411 | 412 | '@nodelib/fs.walk@1.2.8': 413 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 414 | engines: {node: '>= 8'} 415 | 416 | '@pkgr/core@0.2.4': 417 | resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} 418 | engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} 419 | 420 | '@selderee/plugin-htmlparser2@0.11.0': 421 | resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} 422 | 423 | '@types/estree@1.0.7': 424 | resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} 425 | 426 | '@types/json-schema@7.0.15': 427 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 428 | 429 | '@types/mailparser@3.4.5': 430 | resolution: {integrity: sha512-EPERBp7fLeFZh7tS2X36MF7jawUx3Y6/0rXciZah3CTYgwLi3e0kpGUJ6FOmUabgzis/U1g+3/JzrVWbWIOGjg==} 431 | 432 | '@types/node@20.17.31': 433 | resolution: {integrity: sha512-quODOCNXQAbNf1Q7V+fI8WyErOCh0D5Yd31vHnKu4GkSztGQ7rlltAaqXhHhLl33tlVyUXs2386MkANSwgDn6A==} 434 | 435 | '@types/nodemailer@6.4.17': 436 | resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} 437 | 438 | '@types/smtp-server@3.5.10': 439 | resolution: {integrity: sha512-i3Jx7sJ2qF52vjaOf3HguulXlWRFf6BSfsRLsIdmytDyVGv7KkhSs+gR9BXJnJWg1Ljkh/56Fh1Xqwa6u6X7zw==} 440 | 441 | '@typescript-eslint/eslint-plugin@8.31.0': 442 | resolution: {integrity: sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==} 443 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 444 | peerDependencies: 445 | '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 446 | eslint: ^8.57.0 || ^9.0.0 447 | typescript: '>=4.8.4 <5.9.0' 448 | 449 | '@typescript-eslint/parser@8.31.0': 450 | resolution: {integrity: sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==} 451 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 452 | peerDependencies: 453 | eslint: ^8.57.0 || ^9.0.0 454 | typescript: '>=4.8.4 <5.9.0' 455 | 456 | '@typescript-eslint/scope-manager@8.31.0': 457 | resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==} 458 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 459 | 460 | '@typescript-eslint/type-utils@8.31.0': 461 | resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==} 462 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 463 | peerDependencies: 464 | eslint: ^8.57.0 || ^9.0.0 465 | typescript: '>=4.8.4 <5.9.0' 466 | 467 | '@typescript-eslint/types@8.31.0': 468 | resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==} 469 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 470 | 471 | '@typescript-eslint/typescript-estree@8.31.0': 472 | resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==} 473 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 474 | peerDependencies: 475 | typescript: '>=4.8.4 <5.9.0' 476 | 477 | '@typescript-eslint/utils@8.31.0': 478 | resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==} 479 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 480 | peerDependencies: 481 | eslint: ^8.57.0 || ^9.0.0 482 | typescript: '>=4.8.4 <5.9.0' 483 | 484 | '@typescript-eslint/visitor-keys@8.31.0': 485 | resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==} 486 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 487 | 488 | acorn-jsx@5.3.2: 489 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 490 | peerDependencies: 491 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 492 | 493 | acorn@8.14.1: 494 | resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} 495 | engines: {node: '>=0.4.0'} 496 | hasBin: true 497 | 498 | ajv@6.12.6: 499 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 500 | 501 | ansi-escapes@4.3.2: 502 | resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} 503 | engines: {node: '>=8'} 504 | 505 | ansi-regex@5.0.1: 506 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 507 | engines: {node: '>=8'} 508 | 509 | ansi-styles@4.3.0: 510 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 511 | engines: {node: '>=8'} 512 | 513 | argparse@2.0.1: 514 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 515 | 516 | balanced-match@1.0.2: 517 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 518 | 519 | base32.js@0.1.0: 520 | resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} 521 | engines: {node: '>=0.12.0'} 522 | 523 | brace-expansion@1.1.11: 524 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 525 | 526 | brace-expansion@2.0.1: 527 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 528 | 529 | braces@3.0.3: 530 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 531 | engines: {node: '>=8'} 532 | 533 | callsites@3.1.0: 534 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 535 | engines: {node: '>=6'} 536 | 537 | chalk@4.1.2: 538 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 539 | engines: {node: '>=10'} 540 | 541 | chardet@0.7.0: 542 | resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} 543 | 544 | cli-width@4.1.0: 545 | resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} 546 | engines: {node: '>= 12'} 547 | 548 | color-convert@2.0.1: 549 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 550 | engines: {node: '>=7.0.0'} 551 | 552 | color-name@1.1.4: 553 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 554 | 555 | commander@11.1.0: 556 | resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} 557 | engines: {node: '>=16'} 558 | 559 | concat-map@0.0.1: 560 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 561 | 562 | cross-spawn@7.0.6: 563 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 564 | engines: {node: '>= 8'} 565 | 566 | debug@4.4.0: 567 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 568 | engines: {node: '>=6.0'} 569 | peerDependencies: 570 | supports-color: '*' 571 | peerDependenciesMeta: 572 | supports-color: 573 | optional: true 574 | 575 | deep-is@0.1.4: 576 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 577 | 578 | deepmerge@4.3.1: 579 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 580 | engines: {node: '>=0.10.0'} 581 | 582 | dkim-key@1.3.0: 583 | resolution: {integrity: sha512-19+M28U4pucj6R5OfOBpWjNwUNouQUJBEN/w20MIwOVodF17kDMr03rQxvk0m8V+JRrx3x3DGyexvXgubjo5dw==} 584 | 585 | dkim-signature@1.3.0: 586 | resolution: {integrity: sha512-/NPCgG9L+k1+QhCWkGO6QlTavFg1AuhutYE197O/TsQb8OrJk52UJMMBL8eHzZ8Ei8SaUVCeoluJJunQrvz5Vg==} 587 | 588 | dkim@0.8.0: 589 | resolution: {integrity: sha512-lFkIcyPLnd9CqAsY3jHylQh89RL8d5enRgHHr0vMkuhNCe4fS91E+TQnJPvLK/2EFDLW6gBHTAKAU7aYzrBAhQ==} 590 | 591 | dom-serializer@2.0.0: 592 | resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 593 | 594 | domelementtype@2.3.0: 595 | resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} 596 | 597 | domhandler@5.0.3: 598 | resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} 599 | engines: {node: '>= 4'} 600 | 601 | domutils@3.2.2: 602 | resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 603 | 604 | emoji-regex@8.0.0: 605 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 606 | 607 | encoding-japanese@2.2.0: 608 | resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==} 609 | engines: {node: '>=8.10.0'} 610 | 611 | entities@4.5.0: 612 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 613 | engines: {node: '>=0.12'} 614 | 615 | esbuild@0.25.3: 616 | resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} 617 | engines: {node: '>=18'} 618 | hasBin: true 619 | 620 | escape-string-regexp@4.0.0: 621 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 622 | engines: {node: '>=10'} 623 | 624 | eslint-config-prettier@10.1.2: 625 | resolution: {integrity: sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==} 626 | hasBin: true 627 | peerDependencies: 628 | eslint: '>=7.0.0' 629 | 630 | eslint-plugin-prettier@5.2.6: 631 | resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==} 632 | engines: {node: ^14.18.0 || >=16.0.0} 633 | peerDependencies: 634 | '@types/eslint': '>=8.0.0' 635 | eslint: '>=8.0.0' 636 | eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' 637 | prettier: '>=3.0.0' 638 | peerDependenciesMeta: 639 | '@types/eslint': 640 | optional: true 641 | eslint-config-prettier: 642 | optional: true 643 | 644 | eslint-scope@8.3.0: 645 | resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} 646 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 647 | 648 | eslint-visitor-keys@3.4.3: 649 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 650 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 651 | 652 | eslint-visitor-keys@4.2.0: 653 | resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} 654 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 655 | 656 | eslint@9.25.1: 657 | resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==} 658 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 659 | hasBin: true 660 | peerDependencies: 661 | jiti: '*' 662 | peerDependenciesMeta: 663 | jiti: 664 | optional: true 665 | 666 | espree@10.3.0: 667 | resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} 668 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 669 | 670 | esquery@1.6.0: 671 | resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 672 | engines: {node: '>=0.10'} 673 | 674 | esrecurse@4.3.0: 675 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 676 | engines: {node: '>=4.0'} 677 | 678 | estraverse@5.3.0: 679 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 680 | engines: {node: '>=4.0'} 681 | 682 | esutils@2.0.3: 683 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 684 | engines: {node: '>=0.10.0'} 685 | 686 | external-editor@3.1.0: 687 | resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} 688 | engines: {node: '>=4'} 689 | 690 | fast-deep-equal@3.1.3: 691 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 692 | 693 | fast-diff@1.3.0: 694 | resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} 695 | 696 | fast-glob@3.3.3: 697 | resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 698 | engines: {node: '>=8.6.0'} 699 | 700 | fast-json-stable-stringify@2.1.0: 701 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 702 | 703 | fast-levenshtein@2.0.6: 704 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 705 | 706 | fastq@1.19.1: 707 | resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} 708 | 709 | file-entry-cache@8.0.0: 710 | resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 711 | engines: {node: '>=16.0.0'} 712 | 713 | fill-range@7.1.1: 714 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 715 | engines: {node: '>=8'} 716 | 717 | find-up@5.0.0: 718 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 719 | engines: {node: '>=10'} 720 | 721 | flat-cache@4.0.1: 722 | resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 723 | engines: {node: '>=16'} 724 | 725 | flatted@3.3.3: 726 | resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} 727 | 728 | fsevents@2.3.3: 729 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 730 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 731 | os: [darwin] 732 | 733 | get-tsconfig@4.10.0: 734 | resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} 735 | 736 | glob-parent@5.1.2: 737 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 738 | engines: {node: '>= 6'} 739 | 740 | glob-parent@6.0.2: 741 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 742 | engines: {node: '>=10.13.0'} 743 | 744 | globals@14.0.0: 745 | resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 746 | engines: {node: '>=18'} 747 | 748 | globals@16.0.0: 749 | resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} 750 | engines: {node: '>=18'} 751 | 752 | graphemer@1.4.0: 753 | resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 754 | 755 | has-flag@4.0.0: 756 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 757 | engines: {node: '>=8'} 758 | 759 | he@1.2.0: 760 | resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 761 | hasBin: true 762 | 763 | html-to-text@9.0.5: 764 | resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} 765 | engines: {node: '>=14'} 766 | 767 | htmlparser2@8.0.2: 768 | resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} 769 | 770 | iconv-lite@0.4.24: 771 | resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} 772 | engines: {node: '>=0.10.0'} 773 | 774 | iconv-lite@0.6.3: 775 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 776 | engines: {node: '>=0.10.0'} 777 | 778 | ignore@5.3.2: 779 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 780 | engines: {node: '>= 4'} 781 | 782 | import-fresh@3.3.1: 783 | resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} 784 | engines: {node: '>=6'} 785 | 786 | imurmurhash@0.1.4: 787 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 788 | engines: {node: '>=0.8.19'} 789 | 790 | inquirer@12.6.0: 791 | resolution: {integrity: sha512-3zmmccQd/8o65nPOZJZ+2wqt76Ghw3+LaMrmc6JE/IzcvQhJ1st+QLCOo/iLS85/tILU0myG31a2TAZX0ysAvg==} 792 | engines: {node: '>=18'} 793 | peerDependencies: 794 | '@types/node': '>=18' 795 | peerDependenciesMeta: 796 | '@types/node': 797 | optional: true 798 | 799 | ipv6-normalize@1.0.1: 800 | resolution: {integrity: sha512-Bm6H79i01DjgGTCWjUuCjJ6QDo1HB96PT/xCYuyJUP9WFbVDrLSbG4EZCvOCun2rNswZb0c3e4Jt/ws795esHA==} 801 | 802 | is-extglob@2.1.1: 803 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 804 | engines: {node: '>=0.10.0'} 805 | 806 | is-fullwidth-code-point@3.0.0: 807 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 808 | engines: {node: '>=8'} 809 | 810 | is-glob@4.0.3: 811 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 812 | engines: {node: '>=0.10.0'} 813 | 814 | is-number@7.0.0: 815 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 816 | engines: {node: '>=0.12.0'} 817 | 818 | isexe@2.0.0: 819 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 820 | 821 | js-yaml@4.1.0: 822 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 823 | hasBin: true 824 | 825 | json-buffer@3.0.1: 826 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 827 | 828 | json-schema-traverse@0.4.1: 829 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 830 | 831 | json-stable-stringify-without-jsonify@1.0.1: 832 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 833 | 834 | keyv@4.5.4: 835 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 836 | 837 | leac@0.6.0: 838 | resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} 839 | 840 | levn@0.4.1: 841 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 842 | engines: {node: '>= 0.8.0'} 843 | 844 | libbase64@1.3.0: 845 | resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} 846 | 847 | libmime@5.3.6: 848 | resolution: {integrity: sha512-j9mBC7eiqi6fgBPAGvKCXJKJSIASanYF4EeA4iBzSG0HxQxmXnR3KbyWqTn4CwsKSebqCv2f5XZfAO6sKzgvwA==} 849 | 850 | libqp@2.1.1: 851 | resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==} 852 | 853 | linkify-it@5.0.0: 854 | resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} 855 | 856 | locate-path@6.0.0: 857 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 858 | engines: {node: '>=10'} 859 | 860 | lodash.merge@4.6.2: 861 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 862 | 863 | mailparser@3.7.2: 864 | resolution: {integrity: sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==} 865 | 866 | mailsplit@5.4.2: 867 | resolution: {integrity: sha512-4cczG/3Iu3pyl8JgQ76dKkisurZTmxMrA4dj/e8d2jKYcFTZ7MxOzg1gTioTDMPuFXwTrVuN/gxhkrO7wLg7qA==} 868 | 869 | merge2@1.4.1: 870 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 871 | engines: {node: '>= 8'} 872 | 873 | micromatch@4.0.8: 874 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 875 | engines: {node: '>=8.6'} 876 | 877 | minimatch@3.1.2: 878 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 879 | 880 | minimatch@9.0.5: 881 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 882 | engines: {node: '>=16 || 14 >=14.17'} 883 | 884 | ms@2.1.3: 885 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 886 | 887 | mute-stream@2.0.0: 888 | resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} 889 | engines: {node: ^18.17.0 || >=20.5.0} 890 | 891 | natural-compare@1.4.0: 892 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 893 | 894 | nodemailer@6.10.1: 895 | resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==} 896 | engines: {node: '>=6.0.0'} 897 | 898 | nodemailer@6.9.15: 899 | resolution: {integrity: sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==} 900 | engines: {node: '>=6.0.0'} 901 | 902 | nodemailer@6.9.16: 903 | resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==} 904 | engines: {node: '>=6.0.0'} 905 | 906 | optionator@0.9.4: 907 | resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 908 | engines: {node: '>= 0.8.0'} 909 | 910 | os-tmpdir@1.0.2: 911 | resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} 912 | engines: {node: '>=0.10.0'} 913 | 914 | p-limit@3.1.0: 915 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 916 | engines: {node: '>=10'} 917 | 918 | p-locate@5.0.0: 919 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 920 | engines: {node: '>=10'} 921 | 922 | parent-module@1.0.1: 923 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 924 | engines: {node: '>=6'} 925 | 926 | parseley@0.12.1: 927 | resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} 928 | 929 | path-exists@4.0.0: 930 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 931 | engines: {node: '>=8'} 932 | 933 | path-key@3.1.1: 934 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 935 | engines: {node: '>=8'} 936 | 937 | peberminta@0.9.0: 938 | resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} 939 | 940 | picomatch@2.3.1: 941 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 942 | engines: {node: '>=8.6'} 943 | 944 | prelude-ls@1.2.1: 945 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 946 | engines: {node: '>= 0.8.0'} 947 | 948 | prettier-linter-helpers@1.0.0: 949 | resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} 950 | engines: {node: '>=6.0.0'} 951 | 952 | prettier@3.5.3: 953 | resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} 954 | engines: {node: '>=14'} 955 | hasBin: true 956 | 957 | punycode.js@2.3.1: 958 | resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} 959 | engines: {node: '>=6'} 960 | 961 | punycode@2.3.1: 962 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 963 | engines: {node: '>=6'} 964 | 965 | queue-microtask@1.2.3: 966 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 967 | 968 | resolve-from@4.0.0: 969 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 970 | engines: {node: '>=4'} 971 | 972 | resolve-pkg-maps@1.0.0: 973 | resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 974 | 975 | reusify@1.1.0: 976 | resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 977 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 978 | 979 | run-async@3.0.0: 980 | resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} 981 | engines: {node: '>=0.12.0'} 982 | 983 | run-parallel@1.2.0: 984 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 985 | 986 | rxjs@7.8.2: 987 | resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} 988 | 989 | safer-buffer@2.1.2: 990 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 991 | 992 | selderee@0.11.0: 993 | resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} 994 | 995 | semver@7.7.1: 996 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 997 | engines: {node: '>=10'} 998 | hasBin: true 999 | 1000 | shebang-command@2.0.0: 1001 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1002 | engines: {node: '>=8'} 1003 | 1004 | shebang-regex@3.0.0: 1005 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1006 | engines: {node: '>=8'} 1007 | 1008 | signal-exit@4.1.0: 1009 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1010 | engines: {node: '>=14'} 1011 | 1012 | smtp-server@3.13.6: 1013 | resolution: {integrity: sha512-dqbSPKn3PCq3Gp5hxBM99u7PET7cQSAWrauhtArJbc+zrf5xNEOjm9+Ob3lySySrRoIEvNE0dz+w2H/xWFJNRw==} 1014 | engines: {node: '>=12.0.0'} 1015 | 1016 | string-width@4.2.3: 1017 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1018 | engines: {node: '>=8'} 1019 | 1020 | strip-ansi@6.0.1: 1021 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1022 | engines: {node: '>=8'} 1023 | 1024 | strip-json-comments@3.1.1: 1025 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1026 | engines: {node: '>=8'} 1027 | 1028 | supports-color@7.2.0: 1029 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1030 | engines: {node: '>=8'} 1031 | 1032 | synckit@0.11.4: 1033 | resolution: {integrity: sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==} 1034 | engines: {node: ^14.18.0 || >=16.0.0} 1035 | 1036 | tlds@1.255.0: 1037 | resolution: {integrity: sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==} 1038 | hasBin: true 1039 | 1040 | tmp@0.0.33: 1041 | resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} 1042 | engines: {node: '>=0.6.0'} 1043 | 1044 | to-regex-range@5.0.1: 1045 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1046 | engines: {node: '>=8.0'} 1047 | 1048 | ts-api-utils@2.1.0: 1049 | resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} 1050 | engines: {node: '>=18.12'} 1051 | peerDependencies: 1052 | typescript: '>=4.8.4' 1053 | 1054 | tslib@2.8.1: 1055 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1056 | 1057 | tsx@4.19.3: 1058 | resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} 1059 | engines: {node: '>=18.0.0'} 1060 | hasBin: true 1061 | 1062 | type-check@0.4.0: 1063 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 1064 | engines: {node: '>= 0.8.0'} 1065 | 1066 | type-fest@0.21.3: 1067 | resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} 1068 | engines: {node: '>=10'} 1069 | 1070 | typescript@5.8.3: 1071 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 1072 | engines: {node: '>=14.17'} 1073 | hasBin: true 1074 | 1075 | uc.micro@2.1.0: 1076 | resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} 1077 | 1078 | undici-types@6.19.8: 1079 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 1080 | 1081 | uri-js@4.4.1: 1082 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1083 | 1084 | which@2.0.2: 1085 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1086 | engines: {node: '>= 8'} 1087 | hasBin: true 1088 | 1089 | word-wrap@1.2.5: 1090 | resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 1091 | engines: {node: '>=0.10.0'} 1092 | 1093 | wrap-ansi@6.2.0: 1094 | resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} 1095 | engines: {node: '>=8'} 1096 | 1097 | yocto-queue@0.1.0: 1098 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1099 | engines: {node: '>=10'} 1100 | 1101 | yoctocolors-cjs@2.1.2: 1102 | resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} 1103 | engines: {node: '>=18'} 1104 | 1105 | snapshots: 1106 | 1107 | '@esbuild/aix-ppc64@0.25.3': 1108 | optional: true 1109 | 1110 | '@esbuild/android-arm64@0.25.3': 1111 | optional: true 1112 | 1113 | '@esbuild/android-arm@0.25.3': 1114 | optional: true 1115 | 1116 | '@esbuild/android-x64@0.25.3': 1117 | optional: true 1118 | 1119 | '@esbuild/darwin-arm64@0.25.3': 1120 | optional: true 1121 | 1122 | '@esbuild/darwin-x64@0.25.3': 1123 | optional: true 1124 | 1125 | '@esbuild/freebsd-arm64@0.25.3': 1126 | optional: true 1127 | 1128 | '@esbuild/freebsd-x64@0.25.3': 1129 | optional: true 1130 | 1131 | '@esbuild/linux-arm64@0.25.3': 1132 | optional: true 1133 | 1134 | '@esbuild/linux-arm@0.25.3': 1135 | optional: true 1136 | 1137 | '@esbuild/linux-ia32@0.25.3': 1138 | optional: true 1139 | 1140 | '@esbuild/linux-loong64@0.25.3': 1141 | optional: true 1142 | 1143 | '@esbuild/linux-mips64el@0.25.3': 1144 | optional: true 1145 | 1146 | '@esbuild/linux-ppc64@0.25.3': 1147 | optional: true 1148 | 1149 | '@esbuild/linux-riscv64@0.25.3': 1150 | optional: true 1151 | 1152 | '@esbuild/linux-s390x@0.25.3': 1153 | optional: true 1154 | 1155 | '@esbuild/linux-x64@0.25.3': 1156 | optional: true 1157 | 1158 | '@esbuild/netbsd-arm64@0.25.3': 1159 | optional: true 1160 | 1161 | '@esbuild/netbsd-x64@0.25.3': 1162 | optional: true 1163 | 1164 | '@esbuild/openbsd-arm64@0.25.3': 1165 | optional: true 1166 | 1167 | '@esbuild/openbsd-x64@0.25.3': 1168 | optional: true 1169 | 1170 | '@esbuild/sunos-x64@0.25.3': 1171 | optional: true 1172 | 1173 | '@esbuild/win32-arm64@0.25.3': 1174 | optional: true 1175 | 1176 | '@esbuild/win32-ia32@0.25.3': 1177 | optional: true 1178 | 1179 | '@esbuild/win32-x64@0.25.3': 1180 | optional: true 1181 | 1182 | '@eslint-community/eslint-utils@4.6.1(eslint@9.25.1)': 1183 | dependencies: 1184 | eslint: 9.25.1 1185 | eslint-visitor-keys: 3.4.3 1186 | 1187 | '@eslint-community/regexpp@4.12.1': {} 1188 | 1189 | '@eslint/config-array@0.20.0': 1190 | dependencies: 1191 | '@eslint/object-schema': 2.1.6 1192 | debug: 4.4.0 1193 | minimatch: 3.1.2 1194 | transitivePeerDependencies: 1195 | - supports-color 1196 | 1197 | '@eslint/config-helpers@0.2.1': {} 1198 | 1199 | '@eslint/core@0.13.0': 1200 | dependencies: 1201 | '@types/json-schema': 7.0.15 1202 | 1203 | '@eslint/eslintrc@3.3.1': 1204 | dependencies: 1205 | ajv: 6.12.6 1206 | debug: 4.4.0 1207 | espree: 10.3.0 1208 | globals: 14.0.0 1209 | ignore: 5.3.2 1210 | import-fresh: 3.3.1 1211 | js-yaml: 4.1.0 1212 | minimatch: 3.1.2 1213 | strip-json-comments: 3.1.1 1214 | transitivePeerDependencies: 1215 | - supports-color 1216 | 1217 | '@eslint/js@9.25.1': {} 1218 | 1219 | '@eslint/object-schema@2.1.6': {} 1220 | 1221 | '@eslint/plugin-kit@0.2.8': 1222 | dependencies: 1223 | '@eslint/core': 0.13.0 1224 | levn: 0.4.1 1225 | 1226 | '@humanfs/core@0.19.1': {} 1227 | 1228 | '@humanfs/node@0.16.6': 1229 | dependencies: 1230 | '@humanfs/core': 0.19.1 1231 | '@humanwhocodes/retry': 0.3.1 1232 | 1233 | '@humanwhocodes/module-importer@1.0.1': {} 1234 | 1235 | '@humanwhocodes/retry@0.3.1': {} 1236 | 1237 | '@humanwhocodes/retry@0.4.2': {} 1238 | 1239 | '@inquirer/checkbox@4.1.5(@types/node@20.17.31)': 1240 | dependencies: 1241 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1242 | '@inquirer/figures': 1.0.11 1243 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1244 | ansi-escapes: 4.3.2 1245 | yoctocolors-cjs: 2.1.2 1246 | optionalDependencies: 1247 | '@types/node': 20.17.31 1248 | 1249 | '@inquirer/confirm@5.1.9(@types/node@20.17.31)': 1250 | dependencies: 1251 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1252 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1253 | optionalDependencies: 1254 | '@types/node': 20.17.31 1255 | 1256 | '@inquirer/core@10.1.10(@types/node@20.17.31)': 1257 | dependencies: 1258 | '@inquirer/figures': 1.0.11 1259 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1260 | ansi-escapes: 4.3.2 1261 | cli-width: 4.1.0 1262 | mute-stream: 2.0.0 1263 | signal-exit: 4.1.0 1264 | wrap-ansi: 6.2.0 1265 | yoctocolors-cjs: 2.1.2 1266 | optionalDependencies: 1267 | '@types/node': 20.17.31 1268 | 1269 | '@inquirer/editor@4.2.10(@types/node@20.17.31)': 1270 | dependencies: 1271 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1272 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1273 | external-editor: 3.1.0 1274 | optionalDependencies: 1275 | '@types/node': 20.17.31 1276 | 1277 | '@inquirer/expand@4.0.12(@types/node@20.17.31)': 1278 | dependencies: 1279 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1280 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1281 | yoctocolors-cjs: 2.1.2 1282 | optionalDependencies: 1283 | '@types/node': 20.17.31 1284 | 1285 | '@inquirer/figures@1.0.11': {} 1286 | 1287 | '@inquirer/input@4.1.9(@types/node@20.17.31)': 1288 | dependencies: 1289 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1290 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1291 | optionalDependencies: 1292 | '@types/node': 20.17.31 1293 | 1294 | '@inquirer/number@3.0.12(@types/node@20.17.31)': 1295 | dependencies: 1296 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1297 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1298 | optionalDependencies: 1299 | '@types/node': 20.17.31 1300 | 1301 | '@inquirer/password@4.0.12(@types/node@20.17.31)': 1302 | dependencies: 1303 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1304 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1305 | ansi-escapes: 4.3.2 1306 | optionalDependencies: 1307 | '@types/node': 20.17.31 1308 | 1309 | '@inquirer/prompts@7.5.0(@types/node@20.17.31)': 1310 | dependencies: 1311 | '@inquirer/checkbox': 4.1.5(@types/node@20.17.31) 1312 | '@inquirer/confirm': 5.1.9(@types/node@20.17.31) 1313 | '@inquirer/editor': 4.2.10(@types/node@20.17.31) 1314 | '@inquirer/expand': 4.0.12(@types/node@20.17.31) 1315 | '@inquirer/input': 4.1.9(@types/node@20.17.31) 1316 | '@inquirer/number': 3.0.12(@types/node@20.17.31) 1317 | '@inquirer/password': 4.0.12(@types/node@20.17.31) 1318 | '@inquirer/rawlist': 4.1.0(@types/node@20.17.31) 1319 | '@inquirer/search': 3.0.12(@types/node@20.17.31) 1320 | '@inquirer/select': 4.2.0(@types/node@20.17.31) 1321 | optionalDependencies: 1322 | '@types/node': 20.17.31 1323 | 1324 | '@inquirer/rawlist@4.1.0(@types/node@20.17.31)': 1325 | dependencies: 1326 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1327 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1328 | yoctocolors-cjs: 2.1.2 1329 | optionalDependencies: 1330 | '@types/node': 20.17.31 1331 | 1332 | '@inquirer/search@3.0.12(@types/node@20.17.31)': 1333 | dependencies: 1334 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1335 | '@inquirer/figures': 1.0.11 1336 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1337 | yoctocolors-cjs: 2.1.2 1338 | optionalDependencies: 1339 | '@types/node': 20.17.31 1340 | 1341 | '@inquirer/select@4.2.0(@types/node@20.17.31)': 1342 | dependencies: 1343 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1344 | '@inquirer/figures': 1.0.11 1345 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1346 | ansi-escapes: 4.3.2 1347 | yoctocolors-cjs: 2.1.2 1348 | optionalDependencies: 1349 | '@types/node': 20.17.31 1350 | 1351 | '@inquirer/type@3.0.6(@types/node@20.17.31)': 1352 | optionalDependencies: 1353 | '@types/node': 20.17.31 1354 | 1355 | '@nodelib/fs.scandir@2.1.5': 1356 | dependencies: 1357 | '@nodelib/fs.stat': 2.0.5 1358 | run-parallel: 1.2.0 1359 | 1360 | '@nodelib/fs.stat@2.0.5': {} 1361 | 1362 | '@nodelib/fs.walk@1.2.8': 1363 | dependencies: 1364 | '@nodelib/fs.scandir': 2.1.5 1365 | fastq: 1.19.1 1366 | 1367 | '@pkgr/core@0.2.4': {} 1368 | 1369 | '@selderee/plugin-htmlparser2@0.11.0': 1370 | dependencies: 1371 | domhandler: 5.0.3 1372 | selderee: 0.11.0 1373 | 1374 | '@types/estree@1.0.7': {} 1375 | 1376 | '@types/json-schema@7.0.15': {} 1377 | 1378 | '@types/mailparser@3.4.5': 1379 | dependencies: 1380 | '@types/node': 20.17.31 1381 | iconv-lite: 0.6.3 1382 | 1383 | '@types/node@20.17.31': 1384 | dependencies: 1385 | undici-types: 6.19.8 1386 | 1387 | '@types/nodemailer@6.4.17': 1388 | dependencies: 1389 | '@types/node': 20.17.31 1390 | 1391 | '@types/smtp-server@3.5.10': 1392 | dependencies: 1393 | '@types/node': 20.17.31 1394 | '@types/nodemailer': 6.4.17 1395 | 1396 | '@typescript-eslint/eslint-plugin@8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3)': 1397 | dependencies: 1398 | '@eslint-community/regexpp': 4.12.1 1399 | '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) 1400 | '@typescript-eslint/scope-manager': 8.31.0 1401 | '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) 1402 | '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) 1403 | '@typescript-eslint/visitor-keys': 8.31.0 1404 | eslint: 9.25.1 1405 | graphemer: 1.4.0 1406 | ignore: 5.3.2 1407 | natural-compare: 1.4.0 1408 | ts-api-utils: 2.1.0(typescript@5.8.3) 1409 | typescript: 5.8.3 1410 | transitivePeerDependencies: 1411 | - supports-color 1412 | 1413 | '@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3)': 1414 | dependencies: 1415 | '@typescript-eslint/scope-manager': 8.31.0 1416 | '@typescript-eslint/types': 8.31.0 1417 | '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) 1418 | '@typescript-eslint/visitor-keys': 8.31.0 1419 | debug: 4.4.0 1420 | eslint: 9.25.1 1421 | typescript: 5.8.3 1422 | transitivePeerDependencies: 1423 | - supports-color 1424 | 1425 | '@typescript-eslint/scope-manager@8.31.0': 1426 | dependencies: 1427 | '@typescript-eslint/types': 8.31.0 1428 | '@typescript-eslint/visitor-keys': 8.31.0 1429 | 1430 | '@typescript-eslint/type-utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': 1431 | dependencies: 1432 | '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) 1433 | '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) 1434 | debug: 4.4.0 1435 | eslint: 9.25.1 1436 | ts-api-utils: 2.1.0(typescript@5.8.3) 1437 | typescript: 5.8.3 1438 | transitivePeerDependencies: 1439 | - supports-color 1440 | 1441 | '@typescript-eslint/types@8.31.0': {} 1442 | 1443 | '@typescript-eslint/typescript-estree@8.31.0(typescript@5.8.3)': 1444 | dependencies: 1445 | '@typescript-eslint/types': 8.31.0 1446 | '@typescript-eslint/visitor-keys': 8.31.0 1447 | debug: 4.4.0 1448 | fast-glob: 3.3.3 1449 | is-glob: 4.0.3 1450 | minimatch: 9.0.5 1451 | semver: 7.7.1 1452 | ts-api-utils: 2.1.0(typescript@5.8.3) 1453 | typescript: 5.8.3 1454 | transitivePeerDependencies: 1455 | - supports-color 1456 | 1457 | '@typescript-eslint/utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': 1458 | dependencies: 1459 | '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1) 1460 | '@typescript-eslint/scope-manager': 8.31.0 1461 | '@typescript-eslint/types': 8.31.0 1462 | '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) 1463 | eslint: 9.25.1 1464 | typescript: 5.8.3 1465 | transitivePeerDependencies: 1466 | - supports-color 1467 | 1468 | '@typescript-eslint/visitor-keys@8.31.0': 1469 | dependencies: 1470 | '@typescript-eslint/types': 8.31.0 1471 | eslint-visitor-keys: 4.2.0 1472 | 1473 | acorn-jsx@5.3.2(acorn@8.14.1): 1474 | dependencies: 1475 | acorn: 8.14.1 1476 | 1477 | acorn@8.14.1: {} 1478 | 1479 | ajv@6.12.6: 1480 | dependencies: 1481 | fast-deep-equal: 3.1.3 1482 | fast-json-stable-stringify: 2.1.0 1483 | json-schema-traverse: 0.4.1 1484 | uri-js: 4.4.1 1485 | 1486 | ansi-escapes@4.3.2: 1487 | dependencies: 1488 | type-fest: 0.21.3 1489 | 1490 | ansi-regex@5.0.1: {} 1491 | 1492 | ansi-styles@4.3.0: 1493 | dependencies: 1494 | color-convert: 2.0.1 1495 | 1496 | argparse@2.0.1: {} 1497 | 1498 | balanced-match@1.0.2: {} 1499 | 1500 | base32.js@0.1.0: {} 1501 | 1502 | brace-expansion@1.1.11: 1503 | dependencies: 1504 | balanced-match: 1.0.2 1505 | concat-map: 0.0.1 1506 | 1507 | brace-expansion@2.0.1: 1508 | dependencies: 1509 | balanced-match: 1.0.2 1510 | 1511 | braces@3.0.3: 1512 | dependencies: 1513 | fill-range: 7.1.1 1514 | 1515 | callsites@3.1.0: {} 1516 | 1517 | chalk@4.1.2: 1518 | dependencies: 1519 | ansi-styles: 4.3.0 1520 | supports-color: 7.2.0 1521 | 1522 | chardet@0.7.0: {} 1523 | 1524 | cli-width@4.1.0: {} 1525 | 1526 | color-convert@2.0.1: 1527 | dependencies: 1528 | color-name: 1.1.4 1529 | 1530 | color-name@1.1.4: {} 1531 | 1532 | commander@11.1.0: {} 1533 | 1534 | concat-map@0.0.1: {} 1535 | 1536 | cross-spawn@7.0.6: 1537 | dependencies: 1538 | path-key: 3.1.1 1539 | shebang-command: 2.0.0 1540 | which: 2.0.2 1541 | 1542 | debug@4.4.0: 1543 | dependencies: 1544 | ms: 2.1.3 1545 | 1546 | deep-is@0.1.4: {} 1547 | 1548 | deepmerge@4.3.1: {} 1549 | 1550 | dkim-key@1.3.0: {} 1551 | 1552 | dkim-signature@1.3.0: {} 1553 | 1554 | dkim@0.8.0: 1555 | dependencies: 1556 | dkim-key: 1.3.0 1557 | dkim-signature: 1.3.0 1558 | 1559 | dom-serializer@2.0.0: 1560 | dependencies: 1561 | domelementtype: 2.3.0 1562 | domhandler: 5.0.3 1563 | entities: 4.5.0 1564 | 1565 | domelementtype@2.3.0: {} 1566 | 1567 | domhandler@5.0.3: 1568 | dependencies: 1569 | domelementtype: 2.3.0 1570 | 1571 | domutils@3.2.2: 1572 | dependencies: 1573 | dom-serializer: 2.0.0 1574 | domelementtype: 2.3.0 1575 | domhandler: 5.0.3 1576 | 1577 | emoji-regex@8.0.0: {} 1578 | 1579 | encoding-japanese@2.2.0: {} 1580 | 1581 | entities@4.5.0: {} 1582 | 1583 | esbuild@0.25.3: 1584 | optionalDependencies: 1585 | '@esbuild/aix-ppc64': 0.25.3 1586 | '@esbuild/android-arm': 0.25.3 1587 | '@esbuild/android-arm64': 0.25.3 1588 | '@esbuild/android-x64': 0.25.3 1589 | '@esbuild/darwin-arm64': 0.25.3 1590 | '@esbuild/darwin-x64': 0.25.3 1591 | '@esbuild/freebsd-arm64': 0.25.3 1592 | '@esbuild/freebsd-x64': 0.25.3 1593 | '@esbuild/linux-arm': 0.25.3 1594 | '@esbuild/linux-arm64': 0.25.3 1595 | '@esbuild/linux-ia32': 0.25.3 1596 | '@esbuild/linux-loong64': 0.25.3 1597 | '@esbuild/linux-mips64el': 0.25.3 1598 | '@esbuild/linux-ppc64': 0.25.3 1599 | '@esbuild/linux-riscv64': 0.25.3 1600 | '@esbuild/linux-s390x': 0.25.3 1601 | '@esbuild/linux-x64': 0.25.3 1602 | '@esbuild/netbsd-arm64': 0.25.3 1603 | '@esbuild/netbsd-x64': 0.25.3 1604 | '@esbuild/openbsd-arm64': 0.25.3 1605 | '@esbuild/openbsd-x64': 0.25.3 1606 | '@esbuild/sunos-x64': 0.25.3 1607 | '@esbuild/win32-arm64': 0.25.3 1608 | '@esbuild/win32-ia32': 0.25.3 1609 | '@esbuild/win32-x64': 0.25.3 1610 | 1611 | escape-string-regexp@4.0.0: {} 1612 | 1613 | eslint-config-prettier@10.1.2(eslint@9.25.1): 1614 | dependencies: 1615 | eslint: 9.25.1 1616 | 1617 | eslint-plugin-prettier@5.2.6(eslint-config-prettier@10.1.2(eslint@9.25.1))(eslint@9.25.1)(prettier@3.5.3): 1618 | dependencies: 1619 | eslint: 9.25.1 1620 | prettier: 3.5.3 1621 | prettier-linter-helpers: 1.0.0 1622 | synckit: 0.11.4 1623 | optionalDependencies: 1624 | eslint-config-prettier: 10.1.2(eslint@9.25.1) 1625 | 1626 | eslint-scope@8.3.0: 1627 | dependencies: 1628 | esrecurse: 4.3.0 1629 | estraverse: 5.3.0 1630 | 1631 | eslint-visitor-keys@3.4.3: {} 1632 | 1633 | eslint-visitor-keys@4.2.0: {} 1634 | 1635 | eslint@9.25.1: 1636 | dependencies: 1637 | '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1) 1638 | '@eslint-community/regexpp': 4.12.1 1639 | '@eslint/config-array': 0.20.0 1640 | '@eslint/config-helpers': 0.2.1 1641 | '@eslint/core': 0.13.0 1642 | '@eslint/eslintrc': 3.3.1 1643 | '@eslint/js': 9.25.1 1644 | '@eslint/plugin-kit': 0.2.8 1645 | '@humanfs/node': 0.16.6 1646 | '@humanwhocodes/module-importer': 1.0.1 1647 | '@humanwhocodes/retry': 0.4.2 1648 | '@types/estree': 1.0.7 1649 | '@types/json-schema': 7.0.15 1650 | ajv: 6.12.6 1651 | chalk: 4.1.2 1652 | cross-spawn: 7.0.6 1653 | debug: 4.4.0 1654 | escape-string-regexp: 4.0.0 1655 | eslint-scope: 8.3.0 1656 | eslint-visitor-keys: 4.2.0 1657 | espree: 10.3.0 1658 | esquery: 1.6.0 1659 | esutils: 2.0.3 1660 | fast-deep-equal: 3.1.3 1661 | file-entry-cache: 8.0.0 1662 | find-up: 5.0.0 1663 | glob-parent: 6.0.2 1664 | ignore: 5.3.2 1665 | imurmurhash: 0.1.4 1666 | is-glob: 4.0.3 1667 | json-stable-stringify-without-jsonify: 1.0.1 1668 | lodash.merge: 4.6.2 1669 | minimatch: 3.1.2 1670 | natural-compare: 1.4.0 1671 | optionator: 0.9.4 1672 | transitivePeerDependencies: 1673 | - supports-color 1674 | 1675 | espree@10.3.0: 1676 | dependencies: 1677 | acorn: 8.14.1 1678 | acorn-jsx: 5.3.2(acorn@8.14.1) 1679 | eslint-visitor-keys: 4.2.0 1680 | 1681 | esquery@1.6.0: 1682 | dependencies: 1683 | estraverse: 5.3.0 1684 | 1685 | esrecurse@4.3.0: 1686 | dependencies: 1687 | estraverse: 5.3.0 1688 | 1689 | estraverse@5.3.0: {} 1690 | 1691 | esutils@2.0.3: {} 1692 | 1693 | external-editor@3.1.0: 1694 | dependencies: 1695 | chardet: 0.7.0 1696 | iconv-lite: 0.4.24 1697 | tmp: 0.0.33 1698 | 1699 | fast-deep-equal@3.1.3: {} 1700 | 1701 | fast-diff@1.3.0: {} 1702 | 1703 | fast-glob@3.3.3: 1704 | dependencies: 1705 | '@nodelib/fs.stat': 2.0.5 1706 | '@nodelib/fs.walk': 1.2.8 1707 | glob-parent: 5.1.2 1708 | merge2: 1.4.1 1709 | micromatch: 4.0.8 1710 | 1711 | fast-json-stable-stringify@2.1.0: {} 1712 | 1713 | fast-levenshtein@2.0.6: {} 1714 | 1715 | fastq@1.19.1: 1716 | dependencies: 1717 | reusify: 1.1.0 1718 | 1719 | file-entry-cache@8.0.0: 1720 | dependencies: 1721 | flat-cache: 4.0.1 1722 | 1723 | fill-range@7.1.1: 1724 | dependencies: 1725 | to-regex-range: 5.0.1 1726 | 1727 | find-up@5.0.0: 1728 | dependencies: 1729 | locate-path: 6.0.0 1730 | path-exists: 4.0.0 1731 | 1732 | flat-cache@4.0.1: 1733 | dependencies: 1734 | flatted: 3.3.3 1735 | keyv: 4.5.4 1736 | 1737 | flatted@3.3.3: {} 1738 | 1739 | fsevents@2.3.3: 1740 | optional: true 1741 | 1742 | get-tsconfig@4.10.0: 1743 | dependencies: 1744 | resolve-pkg-maps: 1.0.0 1745 | 1746 | glob-parent@5.1.2: 1747 | dependencies: 1748 | is-glob: 4.0.3 1749 | 1750 | glob-parent@6.0.2: 1751 | dependencies: 1752 | is-glob: 4.0.3 1753 | 1754 | globals@14.0.0: {} 1755 | 1756 | globals@16.0.0: {} 1757 | 1758 | graphemer@1.4.0: {} 1759 | 1760 | has-flag@4.0.0: {} 1761 | 1762 | he@1.2.0: {} 1763 | 1764 | html-to-text@9.0.5: 1765 | dependencies: 1766 | '@selderee/plugin-htmlparser2': 0.11.0 1767 | deepmerge: 4.3.1 1768 | dom-serializer: 2.0.0 1769 | htmlparser2: 8.0.2 1770 | selderee: 0.11.0 1771 | 1772 | htmlparser2@8.0.2: 1773 | dependencies: 1774 | domelementtype: 2.3.0 1775 | domhandler: 5.0.3 1776 | domutils: 3.2.2 1777 | entities: 4.5.0 1778 | 1779 | iconv-lite@0.4.24: 1780 | dependencies: 1781 | safer-buffer: 2.1.2 1782 | 1783 | iconv-lite@0.6.3: 1784 | dependencies: 1785 | safer-buffer: 2.1.2 1786 | 1787 | ignore@5.3.2: {} 1788 | 1789 | import-fresh@3.3.1: 1790 | dependencies: 1791 | parent-module: 1.0.1 1792 | resolve-from: 4.0.0 1793 | 1794 | imurmurhash@0.1.4: {} 1795 | 1796 | inquirer@12.6.0(@types/node@20.17.31): 1797 | dependencies: 1798 | '@inquirer/core': 10.1.10(@types/node@20.17.31) 1799 | '@inquirer/prompts': 7.5.0(@types/node@20.17.31) 1800 | '@inquirer/type': 3.0.6(@types/node@20.17.31) 1801 | ansi-escapes: 4.3.2 1802 | mute-stream: 2.0.0 1803 | run-async: 3.0.0 1804 | rxjs: 7.8.2 1805 | optionalDependencies: 1806 | '@types/node': 20.17.31 1807 | 1808 | ipv6-normalize@1.0.1: {} 1809 | 1810 | is-extglob@2.1.1: {} 1811 | 1812 | is-fullwidth-code-point@3.0.0: {} 1813 | 1814 | is-glob@4.0.3: 1815 | dependencies: 1816 | is-extglob: 2.1.1 1817 | 1818 | is-number@7.0.0: {} 1819 | 1820 | isexe@2.0.0: {} 1821 | 1822 | js-yaml@4.1.0: 1823 | dependencies: 1824 | argparse: 2.0.1 1825 | 1826 | json-buffer@3.0.1: {} 1827 | 1828 | json-schema-traverse@0.4.1: {} 1829 | 1830 | json-stable-stringify-without-jsonify@1.0.1: {} 1831 | 1832 | keyv@4.5.4: 1833 | dependencies: 1834 | json-buffer: 3.0.1 1835 | 1836 | leac@0.6.0: {} 1837 | 1838 | levn@0.4.1: 1839 | dependencies: 1840 | prelude-ls: 1.2.1 1841 | type-check: 0.4.0 1842 | 1843 | libbase64@1.3.0: {} 1844 | 1845 | libmime@5.3.6: 1846 | dependencies: 1847 | encoding-japanese: 2.2.0 1848 | iconv-lite: 0.6.3 1849 | libbase64: 1.3.0 1850 | libqp: 2.1.1 1851 | 1852 | libqp@2.1.1: {} 1853 | 1854 | linkify-it@5.0.0: 1855 | dependencies: 1856 | uc.micro: 2.1.0 1857 | 1858 | locate-path@6.0.0: 1859 | dependencies: 1860 | p-locate: 5.0.0 1861 | 1862 | lodash.merge@4.6.2: {} 1863 | 1864 | mailparser@3.7.2: 1865 | dependencies: 1866 | encoding-japanese: 2.2.0 1867 | he: 1.2.0 1868 | html-to-text: 9.0.5 1869 | iconv-lite: 0.6.3 1870 | libmime: 5.3.6 1871 | linkify-it: 5.0.0 1872 | mailsplit: 5.4.2 1873 | nodemailer: 6.9.16 1874 | punycode.js: 2.3.1 1875 | tlds: 1.255.0 1876 | 1877 | mailsplit@5.4.2: 1878 | dependencies: 1879 | libbase64: 1.3.0 1880 | libmime: 5.3.6 1881 | libqp: 2.1.1 1882 | 1883 | merge2@1.4.1: {} 1884 | 1885 | micromatch@4.0.8: 1886 | dependencies: 1887 | braces: 3.0.3 1888 | picomatch: 2.3.1 1889 | 1890 | minimatch@3.1.2: 1891 | dependencies: 1892 | brace-expansion: 1.1.11 1893 | 1894 | minimatch@9.0.5: 1895 | dependencies: 1896 | brace-expansion: 2.0.1 1897 | 1898 | ms@2.1.3: {} 1899 | 1900 | mute-stream@2.0.0: {} 1901 | 1902 | natural-compare@1.4.0: {} 1903 | 1904 | nodemailer@6.10.1: {} 1905 | 1906 | nodemailer@6.9.15: {} 1907 | 1908 | nodemailer@6.9.16: {} 1909 | 1910 | optionator@0.9.4: 1911 | dependencies: 1912 | deep-is: 0.1.4 1913 | fast-levenshtein: 2.0.6 1914 | levn: 0.4.1 1915 | prelude-ls: 1.2.1 1916 | type-check: 0.4.0 1917 | word-wrap: 1.2.5 1918 | 1919 | os-tmpdir@1.0.2: {} 1920 | 1921 | p-limit@3.1.0: 1922 | dependencies: 1923 | yocto-queue: 0.1.0 1924 | 1925 | p-locate@5.0.0: 1926 | dependencies: 1927 | p-limit: 3.1.0 1928 | 1929 | parent-module@1.0.1: 1930 | dependencies: 1931 | callsites: 3.1.0 1932 | 1933 | parseley@0.12.1: 1934 | dependencies: 1935 | leac: 0.6.0 1936 | peberminta: 0.9.0 1937 | 1938 | path-exists@4.0.0: {} 1939 | 1940 | path-key@3.1.1: {} 1941 | 1942 | peberminta@0.9.0: {} 1943 | 1944 | picomatch@2.3.1: {} 1945 | 1946 | prelude-ls@1.2.1: {} 1947 | 1948 | prettier-linter-helpers@1.0.0: 1949 | dependencies: 1950 | fast-diff: 1.3.0 1951 | 1952 | prettier@3.5.3: {} 1953 | 1954 | punycode.js@2.3.1: {} 1955 | 1956 | punycode@2.3.1: {} 1957 | 1958 | queue-microtask@1.2.3: {} 1959 | 1960 | resolve-from@4.0.0: {} 1961 | 1962 | resolve-pkg-maps@1.0.0: {} 1963 | 1964 | reusify@1.1.0: {} 1965 | 1966 | run-async@3.0.0: {} 1967 | 1968 | run-parallel@1.2.0: 1969 | dependencies: 1970 | queue-microtask: 1.2.3 1971 | 1972 | rxjs@7.8.2: 1973 | dependencies: 1974 | tslib: 2.8.1 1975 | 1976 | safer-buffer@2.1.2: {} 1977 | 1978 | selderee@0.11.0: 1979 | dependencies: 1980 | parseley: 0.12.1 1981 | 1982 | semver@7.7.1: {} 1983 | 1984 | shebang-command@2.0.0: 1985 | dependencies: 1986 | shebang-regex: 3.0.0 1987 | 1988 | shebang-regex@3.0.0: {} 1989 | 1990 | signal-exit@4.1.0: {} 1991 | 1992 | smtp-server@3.13.6: 1993 | dependencies: 1994 | base32.js: 0.1.0 1995 | ipv6-normalize: 1.0.1 1996 | nodemailer: 6.9.15 1997 | punycode.js: 2.3.1 1998 | 1999 | string-width@4.2.3: 2000 | dependencies: 2001 | emoji-regex: 8.0.0 2002 | is-fullwidth-code-point: 3.0.0 2003 | strip-ansi: 6.0.1 2004 | 2005 | strip-ansi@6.0.1: 2006 | dependencies: 2007 | ansi-regex: 5.0.1 2008 | 2009 | strip-json-comments@3.1.1: {} 2010 | 2011 | supports-color@7.2.0: 2012 | dependencies: 2013 | has-flag: 4.0.0 2014 | 2015 | synckit@0.11.4: 2016 | dependencies: 2017 | '@pkgr/core': 0.2.4 2018 | tslib: 2.8.1 2019 | 2020 | tlds@1.255.0: {} 2021 | 2022 | tmp@0.0.33: 2023 | dependencies: 2024 | os-tmpdir: 1.0.2 2025 | 2026 | to-regex-range@5.0.1: 2027 | dependencies: 2028 | is-number: 7.0.0 2029 | 2030 | ts-api-utils@2.1.0(typescript@5.8.3): 2031 | dependencies: 2032 | typescript: 5.8.3 2033 | 2034 | tslib@2.8.1: {} 2035 | 2036 | tsx@4.19.3: 2037 | dependencies: 2038 | esbuild: 0.25.3 2039 | get-tsconfig: 4.10.0 2040 | optionalDependencies: 2041 | fsevents: 2.3.3 2042 | 2043 | type-check@0.4.0: 2044 | dependencies: 2045 | prelude-ls: 1.2.1 2046 | 2047 | type-fest@0.21.3: {} 2048 | 2049 | typescript@5.8.3: {} 2050 | 2051 | uc.micro@2.1.0: {} 2052 | 2053 | undici-types@6.19.8: {} 2054 | 2055 | uri-js@4.4.1: 2056 | dependencies: 2057 | punycode: 2.3.1 2058 | 2059 | which@2.0.2: 2060 | dependencies: 2061 | isexe: 2.0.0 2062 | 2063 | word-wrap@1.2.5: {} 2064 | 2065 | wrap-ansi@6.2.0: 2066 | dependencies: 2067 | ansi-styles: 4.3.0 2068 | string-width: 4.2.3 2069 | strip-ansi: 6.0.1 2070 | 2071 | yocto-queue@0.1.0: {} 2072 | 2073 | yoctocolors-cjs@2.1.2: {} 2074 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export interface DkimConfig { 5 | publicKey: string; 6 | privateKey: string; 7 | selector: string; 8 | } 9 | 10 | export interface TinkSESConfig { 11 | port: number; 12 | host: string; 13 | username: string; 14 | password: string; 15 | domain: string; 16 | ip: string[]; 17 | dkim: DkimConfig; 18 | } 19 | 20 | export const defaultConfig: TinkSESConfig = { 21 | port: 25, 22 | host: 'localhost', 23 | username: 'user', 24 | password: 'password', 25 | domain: 'example.com', 26 | ip: [], 27 | dkim: { 28 | privateKey: '', 29 | publicKey: '', 30 | selector: 'default', 31 | }, 32 | }; 33 | 34 | export function loadConfig(configPath: string): TinkSESConfig | null { 35 | try { 36 | const configFile = fs.readFileSync(configPath, 'utf8'); 37 | return JSON.parse(configFile); 38 | } catch (error) { 39 | console.error(`Error reading config file at ${configPath}:`, (error as Error).message); 40 | return null; 41 | } 42 | } 43 | 44 | export function saveConfig(configPath: string, config: TinkSESConfig): void { 45 | const dirPath = path.dirname(configPath); 46 | if (!fs.existsSync(dirPath)) { 47 | fs.mkdirSync(dirPath, { recursive: true }); 48 | } 49 | fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 50 | } 51 | -------------------------------------------------------------------------------- /src/dns-creation.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export function generateDkimKeys( 4 | outputDir: string, 5 | selector: string = 'default' 6 | ): { privateKey: string; publicKey: string } { 7 | console.log('Generating DKIM keys...'); 8 | // Generate key pair 9 | const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { 10 | modulusLength: 2048, 11 | publicKeyEncoding: { 12 | type: 'spki', 13 | format: 'pem', 14 | }, 15 | privateKeyEncoding: { 16 | type: 'pkcs8', 17 | format: 'pem', 18 | }, 19 | }); 20 | 21 | return { 22 | privateKey, 23 | publicKey, 24 | }; 25 | 26 | // Create DNS TXT record 27 | // Convert the public key to the correct format for DNS 28 | /* const publicKeyForDns = publicKey 29 | .toString() 30 | .replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\n/g, '') 31 | .trim(); 32 | 33 | const dnsRecord = `${selector}._domainkey IN TXT "v=DKIM1; k=rsa; p=${publicKeyForDns}"`; 34 | 35 | console.log('DKIM keys generated successfully!'); 36 | console.log(`Private key saved at: ${privateKeyPath}`); 37 | console.log(`Public key saved at: ${publicKeyPath}`); 38 | 39 | return { privateKeyPath, publicKeyPath, dnsRecord }; */ 40 | } 41 | 42 | export function generateSpfRecord(domain: string, ips: string[]): string { 43 | const ipEntries = ips 44 | .map(ip => { 45 | return ip.includes(':') ? `ip6:${ip}` : `ip4:${ip}`; 46 | }) 47 | .join(' '); 48 | 49 | const spfRecord = `v=spf1 ${ipEntries} ${ipEntries ? '~all' : '-all'}`; 50 | 51 | return spfRecord; 52 | } 53 | 54 | export function generateDmarcRecord(domain: string): string { 55 | const dmarcRecord = 'v=DMARC1; p=none; sp=none; adkim=r; aspf=r;'; 56 | return dmarcRecord; 57 | } 58 | -------------------------------------------------------------------------------- /src/dns-verification.ts: -------------------------------------------------------------------------------- 1 | import dns from 'dns'; 2 | import { promisify } from 'util'; 3 | import fs from 'fs'; 4 | import crypto from 'crypto'; 5 | import { TinkSESConfig } from './config.js'; 6 | import { generateSpfRecord, generateDmarcRecord } from './dns-creation.js'; 7 | 8 | // Promisify DNS lookups 9 | const resolveTxt = promisify(dns.resolveTxt); 10 | 11 | interface VerificationResult { 12 | isValid: boolean; 13 | message: string; 14 | } 15 | 16 | /** 17 | * Verify SPF record configuration 18 | * @param domain Domain to check 19 | * @param ips Expected IP addresses in the SPF record 20 | */ 21 | async function verifySpfRecord(domain: string, ips: string[]): Promise { 22 | try { 23 | const records = await resolveTxt(domain); 24 | 25 | // Find SPF record 26 | const spfRecord = records.flat().find(record => record.startsWith('v=spf1')); 27 | 28 | if (!spfRecord) { 29 | return { 30 | isValid: false, 31 | message: `No SPF record found for ${domain}. Please add the SPF record to your DNS configuration.`, 32 | }; 33 | } 34 | 35 | // Check if all IPs are included in the SPF record 36 | const missingIps = ips.filter(ip => { 37 | const format = ip.includes(':') ? `ip6:${ip}` : `ip4:${ip}`; 38 | return !spfRecord.includes(format); 39 | }); 40 | 41 | if (missingIps.length > 0) { 42 | return { 43 | isValid: false, 44 | message: `SPF record found but missing the following IPs: ${missingIps.join(', ')}`, 45 | }; 46 | } 47 | 48 | return { 49 | isValid: true, 50 | message: 'SPF record is properly configured.', 51 | }; 52 | } catch (error) { 53 | return { 54 | isValid: false, 55 | message: `Failed to verify SPF record: ${error instanceof Error ? error.message : String(error)}`, 56 | }; 57 | } 58 | } 59 | 60 | /** 61 | * Verify DKIM record configuration 62 | * @param domain Domain to check 63 | * @param selector DKIM selector 64 | */ 65 | async function verifyDkimRecord(domain: string, selector: string): Promise { 66 | try { 67 | const dkimDomain = `${selector}._domainkey.${domain}`; 68 | const records = await resolveTxt(dkimDomain); 69 | 70 | if (!records || records.length === 0) { 71 | return { 72 | isValid: false, 73 | message: `No DKIM record found for ${dkimDomain}. Please add the DKIM record to your DNS configuration.`, 74 | }; 75 | } 76 | 77 | const dkimRecord = records.flat().find(record => record.startsWith('v=DKIM1')); 78 | 79 | if (!dkimRecord) { 80 | return { 81 | isValid: false, 82 | message: `Invalid DKIM record format for ${dkimDomain}. Record should start with "v=DKIM1".`, 83 | }; 84 | } 85 | 86 | return { 87 | isValid: true, 88 | message: 'DKIM record is properly configured.', 89 | }; 90 | } catch (error) { 91 | return { 92 | isValid: false, 93 | message: `Failed to verify DKIM record: ${error instanceof Error ? error.message : String(error)}`, 94 | }; 95 | } 96 | } 97 | 98 | /** 99 | * Verify DMARC record configuration 100 | * @param domain Domain to check 101 | */ 102 | async function verifyDmarcRecord(domain: string): Promise { 103 | try { 104 | const dmarcDomain = `_dmarc.${domain}`; 105 | const records = await resolveTxt(dmarcDomain); 106 | 107 | if (!records || records.length === 0) { 108 | return { 109 | isValid: false, 110 | message: `No DMARC record found for ${dmarcDomain}. Please add the DMARC record to your DNS configuration.`, 111 | }; 112 | } 113 | 114 | const dmarcRecord = records.flat().find(record => record.startsWith('v=DMARC1')); 115 | 116 | if (!dmarcRecord) { 117 | return { 118 | isValid: false, 119 | message: `Invalid DMARC record format for ${dmarcDomain}. Record should start with "v=DMARC1".`, 120 | }; 121 | } 122 | 123 | return { 124 | isValid: true, 125 | message: 'DMARC record is properly configured.', 126 | }; 127 | } catch (error) { 128 | return { 129 | isValid: false, 130 | message: `Failed to verify DMARC record: ${error instanceof Error ? error.message : String(error)}`, 131 | }; 132 | } 133 | } 134 | 135 | /** 136 | * Generate DNS configuration tips for missing or invalid records 137 | * @param config TinkSES configuration 138 | * @param spf Whether to generate for SPF record 139 | * @param dkim Whether to generate for DKIM record 140 | * @param dmarc Whether to generate for DMARC record 141 | * @returns Configuration tips as a formatted string 142 | */ 143 | export function generateDnsConfigurationTips( 144 | config: TinkSESConfig, 145 | spf: boolean, 146 | dkim: boolean, 147 | dmarc: boolean 148 | ): string { 149 | let tips = '\n=== DNS CONFIGURATION TIPS ===\n'; 150 | 151 | if (spf) { 152 | const spfRecord = generateSpfRecord(config.domain, config.ip); 153 | tips += '\n📌 SPF Record:\n'; 154 | tips += "Add this TXT record to your domain's DNS configuration:\n\n"; 155 | tips += `${config.domain}. IN TXT "${spfRecord}"\n\n`; 156 | tips += 'This allows your server IPs to send mail for your domain.\n'; 157 | } 158 | 159 | if (dkim) { 160 | // If DKIM private key exists, extract public key for DNS record 161 | let dkimTip = '\n📌 DKIM Record:\n'; 162 | if (config.dkim.publicKey) { 163 | try { 164 | const publicKey = config.dkim.publicKey 165 | .replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\n/g, '') 166 | .trim(); 167 | 168 | dkimTip += "Add this TXT record to your domain's DNS configuration:\n\n"; 169 | dkimTip += `${config.dkim.selector}._domainkey.${config.domain}. IN TXT "v=DKIM1; k=rsa; p=${publicKey}"\n\n`; 170 | } catch (error) { 171 | dkimTip += 172 | 'Unable to generate DKIM record from private key. Please run "tinkses init" to generate new keys.\n'; 173 | } 174 | } else { 175 | dkimTip += 'DKIM private key not found. Please run "tinkses init" to generate DKIM keys.\n'; 176 | } 177 | tips += dkimTip; 178 | tips += 'DKIM proves email authenticity and prevents domain spoofing.\n'; 179 | } 180 | 181 | if (dmarc) { 182 | const dmarcRecord = generateDmarcRecord(config.domain); 183 | tips += '\n📌 DMARC Record:\n'; 184 | tips += "Add this TXT record to your domain's DNS configuration:\n\n"; 185 | tips += `_dmarc.${config.domain}. IN TXT "${dmarcRecord}"\n\n`; 186 | tips += 'DMARC tells receivers how to handle emails that fail SPF or DKIM checks.\n'; 187 | } 188 | 189 | tips += "\nOnce you've added these records, DNS changes may take 24-48 hours to propagate.\n"; 190 | tips += 'You can verify your DNS records using tools like https://mxtoolbox.com/\n'; 191 | 192 | return tips; 193 | } 194 | 195 | export async function verifyDnsConfiguration( 196 | config: TinkSESConfig, 197 | strict: boolean = false 198 | ): Promise { 199 | console.log(`\nVerifying DNS configuration for ${config.domain}...`); 200 | 201 | const spfResult = await verifySpfRecord(config.domain, config.ip); 202 | const dkimResult = await verifyDkimRecord(config.domain, config.dkim.selector); 203 | const dmarcResult = await verifyDmarcRecord(config.domain); 204 | 205 | console.log(`\nSPF: ${spfResult.isValid ? '✓' : '✗'} ${spfResult.message}`); 206 | console.log(`DKIM: ${dkimResult.isValid ? '✓' : '✗'} ${dkimResult.message}`); 207 | console.log(`DMARC: ${dmarcResult.isValid ? '✓' : '✗'} ${dmarcResult.message}`); 208 | 209 | const allValid = spfResult.isValid && dkimResult.isValid && dmarcResult.isValid; 210 | 211 | if (!allValid) { 212 | // Display configuration tips for missing or invalid records 213 | const configTips = generateDnsConfigurationTips( 214 | config, 215 | !spfResult.isValid, 216 | !dkimResult.isValid, 217 | !dmarcResult.isValid 218 | ); 219 | console.log(configTips); 220 | 221 | if (strict) { 222 | console.log( 223 | '\nServer start aborted. Please configure your DNS records correctly and try again.' 224 | ); 225 | return false; 226 | } else { 227 | console.log( 228 | '\nContinuing server startup, but emails may be marked as spam or rejected by receivers.' 229 | ); 230 | } 231 | } else { 232 | console.log( 233 | '\nAll DNS records are properly configured! Your emails should have good deliverability.' 234 | ); 235 | } 236 | 237 | return !strict || allValid; 238 | } 239 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Command } from 'commander'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import { fileURLToPath } from 'url'; 6 | import process from 'process'; 7 | import inquirer from 'inquirer'; 8 | import { loadConfig, saveConfig, TinkSESConfig } from './config.js'; 9 | import { SmtpServer } from './smtp-server.js'; 10 | import { generateDkimKeys, generateSpfRecord, generateDmarcRecord } from './dns-creation.js'; 11 | import { getAllIPs, testSmtpConnections, SmtpConnectionResult } from './network.js'; 12 | import { generateDnsConfigurationTips, verifyDnsConfiguration } from './dns-verification.js'; 13 | 14 | // Get directory name from import.meta.url 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = path.dirname(__filename); 17 | 18 | // Package info 19 | const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')); 20 | 21 | // Create CLI 22 | const program = new Command(); 23 | 24 | program 25 | .name('tinkses') 26 | .description('TinkSES - An open-source mail sending service') 27 | .version(packageJson.version) 28 | .option('-c, --config ', 'Path to config file', './tinkses.config.json'); 29 | 30 | // Init command to set up DKIM, detect IP, and generate DNS records 31 | program 32 | .command('init') 33 | .description('Initialize TinkSES configuration') 34 | .action(async options => { 35 | console.log('Initializing TinkSES...'); 36 | 37 | // Load existing config or create new one 38 | const configPath = program.opts().config; 39 | const config = loadConfig(configPath); 40 | 41 | // If no config loaded, use interactive setup 42 | if (!config) { 43 | await initConfig(configPath); 44 | } else { 45 | console.log('\nConfiguration already exists. Please edit the config file directly.'); 46 | } 47 | }); 48 | 49 | // Connection check command to test connectivity to mail providers' SMTP port 25 50 | program 51 | .command('check-smtp') 52 | .description('Test connection to mail providers on SMTP port 25') 53 | .option( 54 | '-p, --providers ', 55 | 'Comma-separated list of SMTP hosts to check', 56 | 'smtp.gmail.com,smtp.mail.yahoo.com,smtp-mail.outlook.com,smtp.office365.com,smtpin.zoho.com' 57 | ) 58 | .action(async options => { 59 | console.log('Testing SMTP connections to mail providers on port 25...\n'); 60 | 61 | const providers = options.providers.split(',').map((host: string) => host.trim()); 62 | 63 | console.log(`Testing connections to: ${providers.join(', ')}`); 64 | const results = await testSmtpConnections(providers); 65 | 66 | // Format and display results 67 | console.log('\nConnection Test Results:'); 68 | console.log('------------------------'); 69 | 70 | // Count successful connections 71 | const successCount = results.filter((r: SmtpConnectionResult) => r.success).length; 72 | 73 | results.forEach((result: SmtpConnectionResult) => { 74 | if (result.success) { 75 | console.log(`✅ ${result.host}: Connected successfully (${result.responseTime}ms)`); 76 | } else { 77 | console.log(`❌ ${result.host}: Connection failed - ${result.error}`); 78 | } 79 | }); 80 | 81 | console.log('\nSummary:'); 82 | console.log(`${successCount} of ${results.length} connections successful`); 83 | 84 | if (successCount === 0) { 85 | console.log('\n⚠️ Warning: Could not connect to any mail providers on port 25.'); 86 | console.log('This may indicate that your ISP or network is blocking outbound SMTP traffic.'); 87 | console.log('This is common for residential and some commercial internet connections.'); 88 | console.log('Consider the following options:'); 89 | console.log('1. Contact your ISP to unblock port 25'); 90 | console.log('2. Use a cloud provider or VPS where port 25 is not blocked'); 91 | console.log('3. Use a smart host or relay that allows connections on alternative ports'); 92 | } 93 | }); 94 | 95 | // Main command to start server 96 | program.action(async () => { 97 | // Load config 98 | const configPath = program.opts().config; 99 | 100 | // Check if config file exists 101 | const configFileExists = fs.existsSync(configPath); 102 | 103 | // Load or create config 104 | const config = loadConfig(configPath); 105 | 106 | if (!configFileExists || !config) { 107 | console.log('TinkSES is not initialized properly. Running interactive initialization...'); 108 | return await initConfig(configPath); 109 | } 110 | 111 | console.log('Starting TinkSES server...'); 112 | 113 | // Verify DNS configuration before starting 114 | const verificationResult = await verifyDnsConfiguration(config, false); 115 | 116 | // Add option to verify DNS in strict mode 117 | if (process.env.TINKSES_STRICT_DNS_CHECK === 'true' && !verificationResult) { 118 | console.error('DNS verification failed in strict mode. Server startup aborted.'); 119 | process.exit(1); 120 | } 121 | 122 | // Create SMTP server 123 | const smtpServer = new SmtpServer(config); 124 | smtpServer.start(); 125 | 126 | // Handle shutdown 127 | const shutdown = async () => { 128 | console.log('Shutting down TinkSES...'); 129 | await smtpServer.stop(); 130 | process.exit(0); 131 | }; 132 | 133 | // Handle signals for graceful shutdown 134 | ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => { 135 | process.on(signal, shutdown); 136 | }); 137 | }); 138 | 139 | async function initConfig(configPath: string) { 140 | let updatedConfig: TinkSESConfig = await runInteractiveSetup(); 141 | updatedConfig = await completeInitialization(updatedConfig, configPath); 142 | console.log(generateDnsConfigurationTips(updatedConfig, true, true, true)); 143 | console.log('\nInitialization complete! You can now start TinkSES with:'); 144 | console.log(`npx tinkses -c ${configPath}`); 145 | } 146 | 147 | /** 148 | * Run interactive setup to configure TinkSES 149 | * @returns configuration 150 | */ 151 | async function runInteractiveSetup() { 152 | const answers = await inquirer.prompt([ 153 | { 154 | type: 'input', 155 | name: 'domain', 156 | message: 'What domain name will you use for sending emails?', 157 | default: 'example.com', 158 | validate: input => { 159 | if (!input || input === 'example.com') { 160 | return 'Please enter a valid domain name'; 161 | } 162 | return true; 163 | }, 164 | }, 165 | { 166 | type: 'input', 167 | name: 'selector', 168 | message: 'What DKIM selector would you like to use?', 169 | default: 'default', 170 | }, 171 | { 172 | type: 'input', 173 | name: 'port', 174 | message: 'What port should the SMTP server run on?', 175 | default: '2525', 176 | validate: input => { 177 | const port = parseInt(input); 178 | if (isNaN(port) || port < 1 || port > 65535) { 179 | return 'Please enter a valid port number (1-65535)'; 180 | } 181 | return true; 182 | }, 183 | }, 184 | { 185 | type: 'input', 186 | name: 'host', 187 | message: 'What host should the SMTP server bind to?', 188 | default: '127.0.0.1', 189 | }, 190 | { 191 | type: 'input', 192 | name: 'username', 193 | message: 'Set SMTP authentication username:', 194 | default: 'user', 195 | validate: input => (input ? true : 'Username cannot be empty'), 196 | }, 197 | { 198 | type: 'password', 199 | name: 'password', 200 | message: 'Set SMTP authentication password:', 201 | mask: '*', 202 | validate: input => (input ? true : 'Password cannot be empty'), 203 | }, 204 | ]); 205 | 206 | // Update config with user answers 207 | return { 208 | domain: answers.domain, 209 | port: parseInt(answers.port), 210 | host: answers.host, 211 | username: answers.username, 212 | password: answers.password, 213 | ip: [], 214 | dkim: { 215 | privateKey: '', 216 | publicKey: '', 217 | selector: answers.selector, 218 | }, 219 | }; 220 | } 221 | 222 | /** 223 | * Complete the initialization process by generating keys and DNS records 224 | * @param config Configuration 225 | * @param configPath Path to save config 226 | */ 227 | async function completeInitialization(config: TinkSESConfig, configPath: string) { 228 | // Create output directory if it doesn't exist 229 | const resolvedOutputDir = path.dirname(path.resolve(configPath)); 230 | if (!fs.existsSync(resolvedOutputDir)) { 231 | fs.mkdirSync(resolvedOutputDir, { recursive: true }); 232 | } 233 | 234 | // Generate DKIM keys 235 | const { privateKey, publicKey } = generateDkimKeys(resolvedOutputDir, config.dkim.selector); 236 | 237 | // Update config with DKIM settings 238 | config.dkim.privateKey = privateKey; 239 | config.dkim.publicKey = publicKey; 240 | 241 | // Detect IP addresses 242 | console.log('\nDetecting IP addresses...'); 243 | const ips = await getAllIPs(false); 244 | console.log(`Found ${ips.length} IP addresses:`); 245 | ips.forEach((ip: string) => console.log(` - ${ip}`)); 246 | 247 | // Update config with IP addresses 248 | config.ip = ips; 249 | 250 | // Save updated config 251 | saveConfig(configPath, config); 252 | console.log(`\nConfiguration saved to ${configPath}`); 253 | 254 | return config; 255 | } 256 | 257 | // Parse CLI args and execute 258 | program.parse(process.argv); 259 | -------------------------------------------------------------------------------- /src/network.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | export interface NetworkInterface { 4 | name: string; 5 | ipv4: string[]; 6 | ipv6: string[]; 7 | } 8 | 9 | /** 10 | * Check if an IP address is a private/LAN IP 11 | */ 12 | export function isPrivateIP(ip: string): boolean { 13 | // IPv4 private ranges 14 | if (ip.includes('.')) { 15 | const parts = ip.split('.').map(Number); 16 | 17 | // 10.0.0.0 - 10.255.255.255 18 | if (parts[0] === 10) return true; 19 | 20 | // 172.16.0.0 - 172.31.255.255 21 | if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; 22 | 23 | // 192.168.0.0 - 192.168.255.255 24 | if (parts[0] === 192 && parts[1] === 168) return true; 25 | 26 | // 169.254.0.0 - 169.254.255.255 (link-local) 27 | if (parts[0] === 169 && parts[1] === 254) return true; 28 | 29 | // 127.0.0.0 - 127.255.255.255 (loopback) 30 | if (parts[0] === 127) return true; 31 | 32 | // 100.64.0.0/10 (CGNAT range used by Tailscale and ISPs) 33 | if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true; 34 | 35 | // 100.100.100.100 (Tailscale Quad100 address) 36 | if (parts[0] === 100 && parts[1] === 100 && parts[2] === 100 && parts[3] === 100) return true; 37 | } 38 | // IPv6 private ranges 39 | else if (ip.includes(':')) { 40 | // fe80::/10 (link-local) 41 | if ( 42 | ip.toLowerCase().startsWith('fe8') || 43 | ip.toLowerCase().startsWith('fe9') || 44 | ip.toLowerCase().startsWith('fea') || 45 | ip.toLowerCase().startsWith('feb') 46 | ) 47 | return true; 48 | 49 | // fc00::/7 (unique local addresses) 50 | if (ip.toLowerCase().startsWith('fc') || ip.toLowerCase().startsWith('fd')) return true; 51 | 52 | // ::1 (loopback) 53 | if (ip === '::1') return true; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | export function getNetworkInterfaces(includePrivate: boolean = true): NetworkInterface[] { 60 | const interfaces = os.networkInterfaces(); 61 | const result: NetworkInterface[] = []; 62 | 63 | for (const [name, netInterface] of Object.entries(interfaces)) { 64 | if (!netInterface) continue; 65 | 66 | const ipv4: string[] = []; 67 | const ipv6: string[] = []; 68 | 69 | for (const iface of netInterface) { 70 | // Skip internal/loopback interfaces for external use 71 | if (!iface.internal) { 72 | const isPrivate = isPrivateIP(iface.address); 73 | if (includePrivate || !isPrivate) { 74 | if (iface.family === 'IPv4') { 75 | ipv4.push(iface.address); 76 | } else if (iface.family === 'IPv6') { 77 | ipv6.push(iface.address); 78 | } 79 | } 80 | } 81 | } 82 | 83 | if (ipv4.length > 0 || ipv6.length > 0) { 84 | result.push({ name, ipv4, ipv6 }); 85 | } 86 | } 87 | 88 | return result; 89 | } 90 | 91 | export interface IPInfoResponse { 92 | ip: string; 93 | city?: string; 94 | region?: string; 95 | country?: string; 96 | loc?: string; 97 | org?: string; 98 | postal?: string; 99 | timezone?: string; 100 | } 101 | 102 | export async function getPublicIPs(): Promise<{ ipv4?: string; ipv6?: string }> { 103 | const result: { ipv4?: string; ipv6?: string } = {}; 104 | 105 | try { 106 | // Try to get public IPv4 107 | const ipv4Response = await fetch('https://ipinfo.io/json'); 108 | if (ipv4Response.ok) { 109 | const data: IPInfoResponse = await ipv4Response.json(); 110 | if (data && data.ip) { 111 | result.ipv4 = data.ip; 112 | } 113 | } 114 | } catch (error) { 115 | console.error('Error getting public IPv4:', error); 116 | } 117 | 118 | try { 119 | // Try to get public IPv6 120 | const ipv6Response = await fetch('https://v6.ipinfo.io/json'); 121 | if (ipv6Response.ok) { 122 | const data: IPInfoResponse = await ipv6Response.json(); 123 | if (data && data.ip) { 124 | result.ipv6 = data.ip; 125 | } 126 | } 127 | } catch (error) { 128 | console.error('Error getting public IPv6:', error); 129 | } 130 | 131 | return result; 132 | } 133 | 134 | export async function getAllIPs(includePrivate: boolean = true): Promise { 135 | const ips: string[] = []; 136 | 137 | // Get local interfaces 138 | const interfaces = getNetworkInterfaces(includePrivate); 139 | for (const iface of interfaces) { 140 | ips.push(...iface.ipv4); 141 | ips.push(...iface.ipv6); 142 | } 143 | 144 | // Try to get public IPs 145 | try { 146 | const publicIPs = await getPublicIPs(); 147 | if (publicIPs.ipv4 && !ips.includes(publicIPs.ipv4)) { 148 | ips.push(publicIPs.ipv4); 149 | } 150 | if (publicIPs.ipv6 && !ips.includes(publicIPs.ipv6)) { 151 | ips.push(publicIPs.ipv6); 152 | } 153 | } catch (error) { 154 | console.error('Error getting public IPs:', error); 155 | } 156 | 157 | // Return unique IPs 158 | return [...new Set(ips)]; 159 | } 160 | 161 | /** 162 | * Test connection to common email providers' SMTP servers on port 25 163 | * @returns Results of connection tests 164 | */ 165 | export interface SmtpConnectionResult { 166 | host: string; 167 | success: boolean; 168 | error?: string; 169 | responseTime?: number; // in ms 170 | } 171 | 172 | export async function testSmtpConnections( 173 | providers: string[] = [ 174 | 'smtp.gmail.com', 175 | 'smtp.mail.yahoo.com', 176 | 'smtp-mail.outlook.com', 177 | 'smtp.office365.com', 178 | 'smtpin.zoho.com.', 179 | ] 180 | ): Promise { 181 | const results: SmtpConnectionResult[] = []; 182 | const port = 25; 183 | 184 | // Using native Node.js net module for raw TCP connections 185 | const net = await import('net'); 186 | 187 | const testConnection = (host: string): Promise => { 188 | return new Promise(resolve => { 189 | const startTime = Date.now(); 190 | const socket = net.createConnection({ host, port }); 191 | const timeout = setTimeout(() => { 192 | socket.destroy(); 193 | resolve({ 194 | host, 195 | success: false, 196 | error: 'Connection timed out after 5000ms', 197 | }); 198 | }, 5000); 199 | 200 | socket.on('connect', () => { 201 | clearTimeout(timeout); 202 | const responseTime = Date.now() - startTime; 203 | socket.end(); 204 | resolve({ 205 | host, 206 | success: true, 207 | responseTime, 208 | }); 209 | }); 210 | 211 | socket.on('error', err => { 212 | clearTimeout(timeout); 213 | resolve({ 214 | host, 215 | success: false, 216 | error: err.message, 217 | }); 218 | }); 219 | }); 220 | }; 221 | 222 | // Run tests in parallel 223 | const connectionPromises = providers.map(provider => testConnection(provider)); 224 | const connectionResults = await Promise.all(connectionPromises); 225 | 226 | return connectionResults; 227 | } 228 | -------------------------------------------------------------------------------- /src/smtp-server.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { SMTPServer, SMTPServerOptions } from 'smtp-server'; 4 | import { simpleParser, HeaderLines, AddressObject } from 'mailparser'; 5 | import nodemailer from 'nodemailer'; 6 | import { DkimConfig, TinkSESConfig } from './config.js'; 7 | import dns from 'dns'; 8 | import { promisify } from 'util'; 9 | import { createRequire } from 'module'; 10 | 11 | import { SendMailOptions } from 'nodemailer'; 12 | 13 | // Define a type for address objects returned by addressparser 14 | interface EmailAddress { 15 | address: string; 16 | name?: string; 17 | } 18 | 19 | // Import addressparser with createRequire since it doesn't have proper types 20 | const require = createRequire(import.meta.url); 21 | const addressparser = require('nodemailer/lib/addressparser'); 22 | 23 | export function createDkimSigner(domain: string, dkimConfig: DkimConfig) { 24 | try { 25 | const privateKey = dkimConfig.privateKey; 26 | if (!privateKey) return undefined; 27 | 28 | return { 29 | domainName: domain, 30 | keySelector: dkimConfig.selector, 31 | privateKey, 32 | }; 33 | } catch (error) { 34 | console.error('Error loading DKIM private key:', error); 35 | return undefined; 36 | } 37 | } 38 | 39 | const resolveMxWithRetry = async (domain: string, retries = 3): Promise => { 40 | let lastError: any; 41 | for (let attempt = 1; attempt <= retries; attempt++) { 42 | try { 43 | return await promisify(dns.resolveMx)(domain); 44 | } catch (err) { 45 | lastError = err; 46 | if (attempt < retries) { 47 | console.warn(`[WARN] MX lookup failed for ${domain} (attempt ${attempt}), retrying...`); 48 | await new Promise(res => setTimeout(res, 200 * attempt)); 49 | } 50 | } 51 | } 52 | throw lastError; 53 | }; 54 | 55 | export class SmtpServer { 56 | private server: SMTPServer; 57 | private config: TinkSESConfig; 58 | 59 | constructor(config: TinkSESConfig) { 60 | this.config = config; 61 | 62 | const options: SMTPServerOptions = { 63 | secure: false, // Changed to false for development 64 | disableReverseLookup: true, 65 | authMethods: ['PLAIN', 'LOGIN'], 66 | allowInsecureAuth: true, 67 | 68 | onAuth: (auth, session, callback) => { 69 | const username = auth.username; 70 | const password = auth.password; 71 | 72 | console.log(`[AUTH] Attempt: ${username}`); 73 | 74 | // Simple authentication check against config 75 | if (username === this.config.username && password === this.config.password) { 76 | console.log(`[AUTH] SUCCESS: User '${username}' authenticated`); 77 | callback(null, { user: username }); 78 | } else { 79 | console.log(`[AUTH] FAILED: Invalid credentials for user '${username}'`); 80 | callback(new Error('Invalid username or password')); 81 | } 82 | }, 83 | 84 | onConnect: (session, callback) => { 85 | console.log(`[CONN] New connection from ${session.remoteAddress}`); 86 | callback(); 87 | }, 88 | 89 | onMailFrom: (address, session, callback) => { 90 | console.log(`[FROM] ${address.address}`); 91 | 92 | // Ensure the from address is from the configured domain 93 | const [, domain] = address.address.split('@'); 94 | if (domain !== this.config.domain) { 95 | console.log(`[ERROR] Domain '${domain}' not allowed, expected '${this.config.domain}'`); 96 | return callback(new Error(`Sending from domain ${domain} not allowed`)); 97 | } 98 | 99 | callback(); 100 | }, 101 | 102 | onRcptTo: (address, session, callback) => { 103 | console.log(`[TO] ${address.address}`); 104 | callback(); 105 | }, 106 | 107 | onData: (stream, session, callback) => { 108 | console.log('[DATA] Receiving message data...'); 109 | 110 | const chunks: Buffer[] = []; 111 | stream.on('data', chunk => { 112 | chunks.push(chunk); 113 | }); 114 | 115 | stream.on('end', async () => { 116 | const messageBuffer = Buffer.concat(chunks); 117 | 118 | try { 119 | // Parse the email 120 | const parsedMail = await simpleParser(messageBuffer); 121 | const from = session.envelope.mailFrom ? session.envelope.mailFrom.address : 'unknown'; 122 | const to = session.envelope.rcptTo.map(rcpt => rcpt.address).join(', '); 123 | const subject = parsedMail.subject || '(No Subject)'; 124 | const messageId = 125 | parsedMail.messageId || 126 | `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.config.domain}>`; 127 | 128 | console.log('┌──────────────────────────────────────────────────────'); 129 | console.log(`│ MESSAGE RECEIVED:`); 130 | console.log(`│ From: ${from}`); 131 | console.log(`│ To: ${to}`); 132 | console.log(`│ Subject: ${subject}`); 133 | console.log(`│ MessageID: ${messageId}`); 134 | console.log('└──────────────────────────────────────────────────────'); 135 | 136 | // Create base mail options 137 | const flattenAddresses = (field: any) => { 138 | if (!field) return ''; 139 | if (Array.isArray(field)) { 140 | return field 141 | .map((addr: any) => (typeof addr === 'string' ? addr : addr.text || addr.address)) 142 | .join(', '); 143 | } 144 | if (typeof field === 'string') return field; 145 | if (field.text) return field.text; 146 | if (field.value && Array.isArray(field.value)) { 147 | return field.value.map((addr: any) => addr.address || addr.text).join(', '); 148 | } 149 | return ''; 150 | }; 151 | 152 | const mailOptions: SendMailOptions = { 153 | from: from, 154 | to: flattenAddresses(parsedMail.to) || to, 155 | cc: flattenAddresses(parsedMail.cc), 156 | bcc: flattenAddresses(parsedMail.bcc), 157 | subject: subject, 158 | [parsedMail.html ? 'html' : 'text']: 159 | parsedMail.html || parsedMail.text || '(no content)', 160 | attachments: 161 | (parsedMail.attachments as unknown as SendMailOptions['attachments']) || [], 162 | messageId: messageId, 163 | /* headers: parsedMail.headerLines 164 | .filter( 165 | header => 166 | !['from', 'to', 'cc', 'bcc', 'subject', 'message-id'].includes( 167 | header.key.toLowerCase() 168 | ) 169 | ) 170 | .map(header => ({ 171 | key: header.key, 172 | value: header.line, 173 | })), */ 174 | }; 175 | 176 | // Get the from address domain for DKIM 177 | const fromAddress: EmailAddress = addressparser(from, { flatten: true })[0]; 178 | const fromDomain = fromAddress.address.split('@')[1]; 179 | 180 | // Create recipient list from envelope or headers 181 | let recipientsStr = to; 182 | if (parsedMail.cc) 183 | recipientsStr += ',' + (typeof parsedMail.cc === 'string' ? parsedMail.cc : to); 184 | if (parsedMail.bcc) 185 | recipientsStr += ',' + (typeof parsedMail.bcc === 'string' ? parsedMail.bcc : ''); 186 | 187 | const recipients: EmailAddress[] = addressparser(recipientsStr, { flatten: true }); 188 | 189 | console.log(`Recipients: ${recipients.map((r: EmailAddress) => r.address).join(', ')}`); 190 | 191 | // Group recipients by domain 192 | const recipientGroups: Record = {}; 193 | for (const recipient of recipients) { 194 | const recipientDomain = recipient.address.split('@')[1]; 195 | if (!recipientGroups[recipientDomain]) { 196 | recipientGroups[recipientDomain] = []; 197 | } 198 | recipientGroups[recipientDomain].push(recipient); 199 | } 200 | 201 | // Create DKIM signer 202 | const dkimSigner = createDkimSigner(this.config.domain, this.config.dkim); 203 | 204 | // Track delivery results 205 | const results = { 206 | success: [] as string[], 207 | failed: [] as { domain: string; error: string }[], 208 | }; 209 | 210 | // Send to each domain group 211 | for (const domain in recipientGroups) { 212 | const domainRecipients = recipientGroups[domain]; 213 | console.log( 214 | `Processing domain: ${domain} with ${domainRecipients.length} recipients` 215 | ); 216 | 217 | const domainMessage = { 218 | envelope: { 219 | from: from, 220 | to: domainRecipients.map(to => to.address), 221 | }, 222 | ...mailOptions, 223 | }; 224 | 225 | try { 226 | // Look up MX records for the domain with retry 227 | const mx = await resolveMxWithRetry(domain, 3); 228 | const priorityMx = mx.sort((a, b) => a.priority - b.priority)[0]; 229 | const mxHost = priorityMx.exchange; 230 | const mxPort = 25; 231 | 232 | console.log(`Using MX record: ${mxHost}:${mxPort} for domain ${domain}`); 233 | 234 | // Create a transport for this specific domain 235 | const transport = nodemailer.createTransport({ 236 | host: mxHost, 237 | port: mxPort, 238 | secure: false, 239 | name: fromDomain, 240 | debug: true, 241 | tls: { 242 | rejectUnauthorized: false, 243 | }, 244 | dkim: dkimSigner, 245 | }); 246 | 247 | // Send the email 248 | const info = await transport.sendMail(domainMessage); 249 | console.log(`[SUCCESS] Email sent to ${domain} (${info.messageId})`); 250 | results.success.push(domain); 251 | } catch (error) { 252 | const errorMessage = error instanceof Error ? error.message : String(error); 253 | console.error(`[ERROR] Failed to send to domain ${domain}:`, error); 254 | results.failed.push({ 255 | domain, 256 | error: errorMessage, 257 | }); 258 | // Continue with other domains even if one fails 259 | } 260 | } 261 | 262 | // Log summary of results 263 | console.log('┌──────────────────────────────────────────────────────'); 264 | console.log(`│ DELIVERY RESULTS:`); 265 | console.log(`│ Success: ${results.success.length} domains`); 266 | console.log(`│ Failed: ${results.failed.length} domains`); 267 | if (results.failed.length > 0) { 268 | console.log(`│ Failed domains:`); 269 | results.failed.forEach(failure => { 270 | console.log(`│ - ${failure.domain}: ${failure.error}`); 271 | }); 272 | } 273 | console.log('└──────────────────────────────────────────────────────'); 274 | 275 | // Return appropriate response based on results 276 | if (results.failed.length > 0) { 277 | if (results.success.length > 0) { 278 | // Partial success 279 | const error = new Error( 280 | `Partial delivery: ${results.success.length} succeeded, ${results.failed.length} failed` 281 | ); 282 | // @ts-ignore - Adding custom properties to Error 283 | error.deliveryResults = results; 284 | callback(error); 285 | } else { 286 | // Complete failure 287 | const error = new Error(`Delivery failed to all ${results.failed.length} domains`); 288 | // @ts-ignore - Adding custom properties to Error 289 | error.deliveryResults = results; 290 | callback(error); 291 | } 292 | } else { 293 | // Complete success 294 | callback(); 295 | } 296 | } catch (error) { 297 | console.log('┌──────────────────────────────────────────────────────'); 298 | console.log(`│ ERROR PROCESSING MESSAGE:`); 299 | console.log(`│ ${error instanceof Error ? error.message : String(error)}`); 300 | console.log('└──────────────────────────────────────────────────────'); 301 | callback(new Error('Error processing message')); 302 | } 303 | }); 304 | }, 305 | }; 306 | 307 | this.server = new SMTPServer(options); 308 | 309 | this.server.on('error', err => { 310 | console.log('┌──────────────────────────────────────────────────────'); 311 | console.log(`│ SMTP SERVER ERROR:`); 312 | console.log(`│ ${err instanceof Error ? err.message : String(err)}`); 313 | console.log('└──────────────────────────────────────────────────────'); 314 | }); 315 | } 316 | 317 | public start(): void { 318 | this.server.listen(this.config.port, this.config.host, () => { 319 | console.log('┌──────────────────────────────────────────────────────'); 320 | console.log(`│ SMTP SERVER STARTED`); 321 | console.log(`│ Listening on: ${this.config.host}:${this.config.port}`); 322 | console.log(`│ Domain: ${this.config.domain}`); 323 | console.log('└──────────────────────────────────────────────────────'); 324 | }); 325 | } 326 | 327 | public stop(): Promise { 328 | return new Promise(resolve => { 329 | this.server.close(() => { 330 | console.log('┌──────────────────────────────────────────────────────'); 331 | console.log(`│ SMTP SERVER STOPPED`); 332 | console.log('└──────────────────────────────────────────────────────'); 333 | resolve(); 334 | }); 335 | }); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "removeComments": false 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | --------------------------------------------------------------------------------