diff --git a/package-lock.json b/package-lock.json index fe17527..7de4d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,12 @@ "version": "1.0.0", "dependencies": { "@aws-sdk/client-dynamodb": "^3.600.0", - "@aws-sdk/client-rds-data": "^3.1037.0", "@aws-sdk/client-s3": "^3.600.0", + "@aws-sdk/client-sqs": "^3.600.0", "@aws-sdk/client-ssm": "^3.600.0", "@aws-sdk/lib-dynamodb": "^3.600.0", - "csv-parse": "^5.5.0" + "csv-parse": "^5.5.0", + "mailparser": "^3.7.1" } }, "node_modules/@aws-crypto/crc32": { @@ -272,56 +273,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-rds-data": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-rds-data/-/client-rds-data-3.1037.0.tgz", - "integrity": "sha512-Q15Y/ZlIw/yQHtohMwb6s9KhiIIneQ1GNrewVAiXMy3mY9hK5D2w4AP7qAPPmOA8P3qypzHogfWs9+0ncsGJOw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", - "@aws-sdk/middleware-host-header": "^3.972.10", - "@aws-sdk/middleware-logger": "^3.972.10", - "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", - "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.8", - "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", - "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.17", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/hash-node": "^4.2.14", - "@smithy/invalid-dependency": "^4.2.14", - "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.49", - "@smithy/util-defaults-mode-node": "^4.2.54", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/client-s3": { "version": "3.1027.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1027.0.tgz", @@ -388,6 +339,58 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-sqs": { + "version": "3.1039.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1039.0.tgz", + "integrity": "sha512-U8rooijGy5OnSdxEaJ/sIxuGPgHfspxE8K/az22eJJ4qnO2XcVMtkHljvbfQG0yL3wd93XaypNSPI52okdTyCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/credential-provider-node": "^3.972.38", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-sdk-sqs": "^3.972.22", + "@aws-sdk/middleware-user-agent": "^3.972.37", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.23", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/md5-js": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-ssm": { "version": "3.1027.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.1027.0.tgz", @@ -440,13 +443,13 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.5.tgz", - "integrity": "sha512-lMPlYlYfQdNZhlkJgnkmESwrY+hNh3PljmZ+37oAqLNdJ6rnILAwFSyc6B3bJeDOtMORNnMQIej0aTRuOlDyhQ==", + "version": "3.974.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.7.tgz", + "integrity": "sha512-YhRC90ofz5oolTJZlA8voU/oUrCB2azi8Usx51k8hhB5LpWbYQMMXKUqSqkoL0Cru+RQJgWTHpAfEDDIwfUhJw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.19", + "@aws-sdk/xml-builder": "^3.972.22", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", @@ -456,7 +459,7 @@ "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -478,12 +481,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.31.tgz", - "integrity": "sha512-X/yGB73LmDW/6MdDJGCDzZBUXnM3ys4vs9l+5ZTJmiEswDdP1OjeoAFlFjVGS9o4KB2wZWQ9KOfdVNSSK6Ep3w==", + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.33.tgz", + "integrity": "sha512-bJV7eViSJV6GSuuN+VIdNVPdwPsNSf75BiC2v5alPrjR/OCcqgKwSZInKbDFz9mNeizldsyf67jt6YSIiv53Cw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.7", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", @@ -494,12 +497,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.33.tgz", - "integrity": "sha512-c0ZF+lwoWVvX5iCaGKL5T/4DnIw88CGqxA0BcBs3U86mIp5EZYPVg+KSPkMXOyokmADvNewiMUfSG2uFwjRp0g==", + "version": "3.972.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.35.tgz", + "integrity": "sha512-x/BQGEIdq0oI+4WxLjKmnQvT7CnF9r8ezdGt7wXwxb7ckHXQz0Zmgxt8v3Ne0JaT3R5YefmuybHX6E8EnsDXyA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.7", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.6.1", @@ -515,19 +518,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.35.tgz", - "integrity": "sha512-jsU4u/cRkKFLKQS0k918FQ27fzXLG5ENiLWQMYE6581zLeI2hWh04ptlrvZMB3wJT/5d+vSzJk74X1CMFr4y8Q==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.37.tgz", + "integrity": "sha512-eUTpmWfd/BKsq9medhCRcu+GRAhFP2Zrn7/2jKDHHOOjCkhrMoTp/t4cEthqFoG7gE0VGp5wUxrXTdvBCmSmJg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-env": "^3.972.31", - "@aws-sdk/credential-provider-http": "^3.972.33", - "@aws-sdk/credential-provider-login": "^3.972.35", - "@aws-sdk/credential-provider-process": "^3.972.31", - "@aws-sdk/credential-provider-sso": "^3.972.35", - "@aws-sdk/credential-provider-web-identity": "^3.972.35", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/credential-provider-env": "^3.972.33", + "@aws-sdk/credential-provider-http": "^3.972.35", + "@aws-sdk/credential-provider-login": "^3.972.37", + "@aws-sdk/credential-provider-process": "^3.972.33", + "@aws-sdk/credential-provider-sso": "^3.972.37", + "@aws-sdk/credential-provider-web-identity": "^3.972.37", + "@aws-sdk/nested-clients": "^3.997.5", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -540,13 +543,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.35.tgz", - "integrity": "sha512-5oa3j0cA50jPqgNhZ9XdJVopuzUf1klRb28/2MfLYWWiPi9DRVvbrBWT+DidbHTT36520VuXZJahQwR+YgSjrg==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.37.tgz", + "integrity": "sha512-Ty68y8ISSC+g5Q3D0K8uAaoINwvfaOslnNpsF/LgVUxyosYXHawcK2yV4HLXDVugiTTYLQfJfcw0ce5meAGkKw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/nested-clients": "^3.997.5", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", @@ -559,17 +562,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.36.tgz", - "integrity": "sha512-4nT2T8Z7vH8KE9EdjEsuIlHpZSlcaK2PrKbQBjuUGU46BCCzF3WvP0u0Uiosni3Ykmmn4rWLVawoOCLotUtCbg==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.38.tgz", + "integrity": "sha512-BQ9XYnBDVxR2HuV5huXYQYF/PZMTsY+EnwfGnCU2cA8Zw63XpkOtPY8WqiMIZMQCrKPQQEiFURS/o9CIolRLqg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.31", - "@aws-sdk/credential-provider-http": "^3.972.33", - "@aws-sdk/credential-provider-ini": "^3.972.35", - "@aws-sdk/credential-provider-process": "^3.972.31", - "@aws-sdk/credential-provider-sso": "^3.972.35", - "@aws-sdk/credential-provider-web-identity": "^3.972.35", + "@aws-sdk/credential-provider-env": "^3.972.33", + "@aws-sdk/credential-provider-http": "^3.972.35", + "@aws-sdk/credential-provider-ini": "^3.972.37", + "@aws-sdk/credential-provider-process": "^3.972.33", + "@aws-sdk/credential-provider-sso": "^3.972.37", + "@aws-sdk/credential-provider-web-identity": "^3.972.37", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -582,12 +585,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.31.tgz", - "integrity": "sha512-eKeT4MXumpBJsrDLCYcSzIkFPVTFn/es7It2oogp2OhU/ic7P/+xzFpQx9ZhwtXS57Mc5S42BPWi7lHmvs/nYg==", + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.33.tgz", + "integrity": "sha512-yfjGksI9WQbdMObb0VeLXqzTLI+a0qXLJT9gCDiv0+X/xjPpI3mTz6a5FibrhpuEKIe0gSgvs3MaoFZy5cx4WA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.7", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -599,14 +602,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.35.tgz", - "integrity": "sha512-bCuBdfnj0KGDMdLp6utMTLiJcFN2ek9EgZinxQZZSc3FxjJ/HSqeqab2cjbnoNfy8RM6suDCsRkmVY1izp9I+A==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.37.tgz", + "integrity": "sha512-fpwE+20ntpp3i9Xb9vUuQfXLDKYHH+5I2V+ZG96SX1nBzrruhy10RXDgmN7t1etOz3c55stlA3TeQASUA451NQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", - "@aws-sdk/token-providers": "3.1036.0", + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/token-providers": "3.1039.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -618,13 +621,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.35.tgz", - "integrity": "sha512-swW6Bwvl8lanyEMtZOWE/oR6yqcRQH4HTQZUVsnDVgoXvRjRywpYpLv2BWwjUFyjPrqsdX6FeTkf4tMSe/qFTQ==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.37.tgz", + "integrity": "sha512-aryawqyebf+3WhAFNHfF62rekFpYtVcVN7dQ89qnAWsa4n5hJst8qBG6gXC24WHtW7Nnhkf9ScYnjwo0Brn3bw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/nested-clients": "^3.997.5", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -820,12 +823,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.34.tgz", - "integrity": "sha512-/UL96JKjsjdodcRRMKl99tLQvK6Oi9ptLC9iU1yiTF/ruaDX0mtBBtnLNZDxIZRJOCVOtB49ed1YaTadqygk8Q==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.36.tgz", + "integrity": "sha512-YhPix+0x/MdQrb1Ug1GDKeS5fqylIy+naz800asX8II4jqfTk2KY2KhmmYCwZcky8YWtRQQwWCGdoqeAnip8Uw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.7", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.17", @@ -844,6 +847,23 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.22.tgz", + "integrity": "sha512-DtR3mEiOUJcnEX/QuXmvbJto6xvQzp2ftnHb29c0aQYdmmzbKf0gsu9ovx1i/yy4ZR6m0rttTucS0iiP32dlGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-ssec": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.9.tgz", @@ -859,18 +879,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.35.tgz", - "integrity": "sha512-hOFWNOjVmOocpRlrU04nYxjMOeoe0Obu5AXEuhB8zblMCPl3cG1hdluQCZERRKFyhMQjwZnDbhSHjoMUjetFGw==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.37.tgz", + "integrity": "sha512-N1oNpdiLoVAWYD3WFBnUi3LlfoDA06ZHo4ozyjbsJNLvILzvt//0CnR8N+CZ0NWeYgVB/5V59ivixHCWCx2ALw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.7", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "tslib": "^2.6.2" }, "engines": { @@ -878,24 +898,24 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.3.tgz", - "integrity": "sha512-SivE6GP228IVgfsrr2c/vqTg95X0Qj39Yw4uIrcddpkUzIltNMoNOR62leHOLhODfjv9K8X2mPTwS69A5kT0nQ==", + "version": "3.997.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.5.tgz", + "integrity": "sha512-jGFr6DxtcMTmzOkG/a0jCZYv4BBDmeNYVeO+/memSoDkYCJu4Y58xviYmzwJfYyIVSts+X/BVjJm1uGBnwHEMg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.7", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.37", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.24", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.23", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -903,7 +923,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -919,7 +939,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -944,12 +964,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.22.tgz", - "integrity": "sha512-/rXhMXteD+BqhFd0nYprAgcZ/KtU+963uftPqd3tiFcFfooHZINXUGtOmo2SQjRVauCTNqIEzkwuSETdZFqTTA==", + "version": "3.996.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.24.tgz", + "integrity": "sha512-amP7tLikppN940wbBFISYqiuzVmpzMS9U3mcgtmVLjX4fdWI/SNCvrXv6ZxfVzTT4cT0rPKOLhFah2xLwzREWw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.34", + "@aws-sdk/middleware-sdk-s3": "^3.972.36", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", @@ -961,13 +981,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1036.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1036.0.tgz", - "integrity": "sha512-aNSJ6jjDYayxN9ZA1JpycVScX93Lx03kKZ1EXt3DGOTahcWVLJj3oLAlop0xKP+vP2Ga2t49p1tEaMkTbCCaZA==", + "version": "3.1039.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1039.0.tgz", + "integrity": "sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.7", + "@aws-sdk/nested-clients": "^3.997.5", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -1059,12 +1079,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.21.tgz", - "integrity": "sha512-Av4UHTcAWgdvbN0IP9pbtf4Qa1+6LtJqQdZWj5pLn5J67w0pnJJAZZ+7JPPcj2KN3378zD2JDM9DwJKEyvyMTQ==", + "version": "3.973.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.23.tgz", + "integrity": "sha512-gGwq8L2Euw0aNG6Ey4EktiAo3fSCVoDy1CaBIthd+oeaKHPXUrNaApMewQ6La5Hv0lcznOtECZaNvYyc5LXXfA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.37", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", @@ -1084,13 +1104,14 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.19.tgz", - "integrity": "sha512-Cw8IOMdBUEIl8ZlhRC3Dc/E64D5B5/8JhV6vhPLiPfJwcRC84S6F8aBOIi/N4vR9ZyA4I5Cc0Ateb/9EHaJXeQ==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", "license": "Apache-2.0", "dependencies": { + "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.7.1", + "fast-xml-parser": "5.7.2", "tslib": "^2.6.2" }, "engines": { @@ -1118,6 +1139,19 @@ ], "license": "MIT" }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/@smithy/chunked-blob-reader": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", @@ -1353,12 +1387,12 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.13.tgz", - "integrity": "sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz", + "integrity": "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1400,19 +1434,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.5.tgz", - "integrity": "sha512-wnYOpB5vATFKWrY2Z9Alb0KhjZI6AbzU6Fbz3Hq2GnURdRYWB4q+qWivQtSTwXcmWUA3MZ6krfwL6Cq5MAbxsA==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", - "@smithy/service-error-classification": "^4.3.0", + "@smithy/service-error-classification": "^4.3.1", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -1532,9 +1566,9 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.0.tgz", - "integrity": "sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1" @@ -1755,12 +1789,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.4.tgz", - "integrity": "sha512-FY1UQQ1VFmMwiYp1GVS4MeaGD5O0blLNYK0xCRHU+mJgeoH/hSY8Ld8sJWKQ6uznkh14HveRGQJncgPyNl9J+A==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.6.tgz", + "integrity": "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.3.0", + "@smithy/service-error-classification": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -1837,6 +1871,41 @@ "node": ">=18.0.0" } }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, + "node_modules/@zone-eu/mailsplit/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@zone-eu/mailsplit/node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, "node_modules/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", @@ -1849,6 +1918,91 @@ "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", + "license": "MIT", + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/fast-xml-builder": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", @@ -1865,9 +2019,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz", - "integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", "funding": [ { "type": "github", @@ -1885,6 +2039,126 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "license": "MIT" + }, + "node_modules/libmime": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.8.tgz", + "integrity": "sha512-ZrCY+Q66mPvasAfjsQ/IgahzoBvfE1VdtGRpo1hwRB1oK3wJKxhKA3GOcd2a6j7AH5eMFccxK9fBoCpRZTf8ng==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.7.2", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/libqp": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", + "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/mailparser": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.8.tgz", + "integrity": "sha512-7jSlFGXiianVnhnb6wdutJFloD34488nrHY7r6FNqwXAhZ7YiJDYrKKTxZJ0oSrXcAPHm8YoYnh97xyGtrBQ3w==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.8", + "encoding-japanese": "2.2.0", + "he": "1.2.0", + "html-to-text": "9.0.5", + "iconv-lite": "0.7.2", + "libmime": "5.3.8", + "linkify-it": "5.0.0", + "nodemailer": "8.0.5", + "punycode.js": "2.3.1", + "tlds": "1.261.0" + } + }, "node_modules/mnemonist": { "version": "0.38.3", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", @@ -1894,12 +2168,34 @@ "obliterator": "^1.6.1" } }, + "node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/obliterator": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", "license": "MIT" }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/path-expression-matcher": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", @@ -1915,6 +2211,42 @@ "node": ">=14.0.0" } }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/strnum": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", @@ -1927,11 +2259,26 @@ ], "license": "MIT" }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 8c49a67..def8b7e 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ ], "dependencies": { "@aws-sdk/client-dynamodb": "^3.600.0", - "@aws-sdk/client-rds-data": "^3.1037.0", "@aws-sdk/client-s3": "^3.600.0", + "@aws-sdk/client-sqs": "^3.600.0", "@aws-sdk/client-ssm": "^3.600.0", "@aws-sdk/lib-dynamodb": "^3.600.0", - "csv-parse": "^5.5.0" + "csv-parse": "^5.5.0", + "mailparser": "^3.7.1" } } diff --git a/src/notifyPayroll.js b/src/notifyPayroll.js deleted file mode 100644 index a63723f..0000000 --- a/src/notifyPayroll.js +++ /dev/null @@ -1,187 +0,0 @@ -import { RDSDataClient, ExecuteStatementCommand } from "@aws-sdk/client-rds-data"; -import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; -import { DynamoDBDocumentClient, GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb"; -import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; - -const rds = new RDSDataClient(); -const ddb = DynamoDBDocumentClient.from(new DynamoDBClient()); -const ssm = new SSMClient(); - -const CLUSTER_ARN = process.env.AURORA_CLUSTER_ARN; -const SECRET_ARN = process.env.AURORA_SECRET_ARN; -const DATABASE = "payroll"; -const TABLE_NAME = process.env.TABLE_NAME; -const CHANNEL_ID = "C0AV5RBMYKU"; - -let cachedToken; -async function getSlackToken() { - if (cachedToken) return cachedToken; - const { Parameter } = await ssm.send( - new GetParameterCommand({ Name: process.env.SLACK_BOT_TOKEN_PARAM, WithDecryption: true }) - ); - cachedToken = Parameter.Value; - return cachedToken; -} - -async function sql(query) { - const { records } = await rds.send( - new ExecuteStatementCommand({ - resourceArn: CLUSTER_ARN, - secretArn: SECRET_ARN, - database: DATABASE, - sql: query, - }) - ); - return records || []; -} - -function extractValue(field) { - if (field.stringValue !== undefined) return field.stringValue; - if (field.longValue !== undefined) return field.longValue; - if (field.doubleValue !== undefined) return field.doubleValue; - if (field.booleanValue !== undefined) return field.booleanValue; - if (field.isNull) return null; - return null; -} - -const formatCurrency = (v) => - new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(Number(v || 0)); - -const formatDate = (ts) => { - const d = new Date(ts); - return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric" }); -}; - -async function getNotifiedPayrolls() { - const result = await ddb.send( - new GetCommand({ TableName: TABLE_NAME, Key: { pk: "PAYROLL_NOTIFIED" } }) - ); - return result.Item?.checkDates || []; -} - -async function markNotified(checkDate) { - const existing = await getNotifiedPayrolls(); - await ddb.send( - new PutCommand({ - TableName: TABLE_NAME, - Item: { pk: "PAYROLL_NOTIFIED", checkDates: [...existing, checkDate] }, - }) - ); -} - -async function buildPayrollMessage(row) { - const checkDate = extractValue(row[0]); - const companyDebit = extractValue(row[1]); - const grossPay = extractValue(row[2]); - const netPay = extractValue(row[3]); - const employerTaxes = extractValue(row[4]); - const reimbursements = extractValue(row[5]); - const benefits = extractValue(row[6]); - - const contractorRows = await sql( - `SELECT COALESCE(SUM(payments_wage_total), 0), COALESCE(SUM(payments_reimbursement), 0) FROM contractor_payments WHERE payments_date = '${checkDate}'` - ); - const contractorWages = contractorRows.length > 0 ? extractValue(contractorRows[0][0]) : 0; - const contractorReimbursements = contractorRows.length > 0 ? extractValue(contractorRows[0][1]) : 0; - const contractorTotal = contractorWages + contractorReimbursements; - - const totalGross = grossPay + contractorTotal; - const cashRequirement = companyDebit + contractorTotal + benefits; - - const blocks = [ - { - type: "header", - text: { type: "plain_text", text: `Payroll Processed — ${formatDate(checkDate)}` }, - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: `*Check Date*\n${formatDate(checkDate)}` }, - { type: "mrkdwn", text: `*Total Gross Pay*\n${formatCurrency(totalGross)}` }, - ], - }, - { type: "divider" }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: `*Employee Gross*\n${formatCurrency(grossPay)}` }, - { type: "mrkdwn", text: `*Contractor Gross*\n${formatCurrency(contractorTotal)}` }, - ], - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: `*Employee Net Pay*\n${formatCurrency(netPay)}` }, - { type: "mrkdwn", text: `*Reimbursements*\n${formatCurrency(reimbursements)}` }, - ], - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: `*Employer Taxes*\n${formatCurrency(employerTaxes)}` }, - { type: "mrkdwn", text: `*Benefits (401k)*\n${formatCurrency(benefits)}` }, - ], - }, - { type: "divider" }, - { - type: "section", - text: { - type: "mrkdwn", - text: `:moneybag: *Total Bank Withdrawal on ${formatDate(checkDate)}: ${formatCurrency(cashRequirement)}*`, - }, - }, - ]; - - return { checkDate, blocks }; -} - -export const handler = async () => { - const payrollRows = await sql( - "SELECT check_date, company_debit, gross_pay, net_pay, employer_taxes, reimbursements, benefits FROM payroll WHERE company_debit > 0 ORDER BY check_date DESC" - ); - - if (payrollRows.length === 0) { - console.log("No processed payrolls found"); - return { statusCode: 200, body: "No payrolls" }; - } - - const notified = await getNotifiedPayrolls(); - const newPayrolls = payrollRows.filter( - (row) => !notified.includes(extractValue(row[0])) - ); - - if (newPayrolls.length === 0) { - console.log("All payrolls already notified"); - return { statusCode: 200, body: "Already notified" }; - } - - const token = await getSlackToken(); - - for (const row of newPayrolls) { - const { checkDate, blocks } = await buildPayrollMessage(row); - - const res = await fetch("https://slack.com/api/chat.postMessage", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - channel: CHANNEL_ID, - blocks, - text: `Payroll processed for ${formatDate(checkDate)}`, - }), - }); - - const data = await res.json(); - if (!data.ok) { - console.error("Slack post failed:", data.error); - throw new Error(`Slack API error: ${data.error}`); - } - - await markNotified(checkDate); - console.log(`Notified for check date: ${checkDate}`); - } - - return { statusCode: 200, body: `Notified ${newPayrolls.length} payroll(s)` }; -}; diff --git a/src/processPayrollEmail.js b/src/processPayrollEmail.js new file mode 100644 index 0000000..dff1bd1 --- /dev/null +++ b/src/processPayrollEmail.js @@ -0,0 +1,464 @@ +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { + DynamoDBDocumentClient, + GetCommand, + PutCommand, + ScanCommand, + BatchWriteCommand, +} from "@aws-sdk/lib-dynamodb"; +import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; +import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; +import { simpleParser } from "mailparser"; + +const s3 = new S3Client(); +const ddb = DynamoDBDocumentClient.from(new DynamoDBClient()); +const sqs = new SQSClient(); +const ssm = new SSMClient(); + +const TABLE_NAME = process.env.TABLE_NAME; +const CHANNEL_ID = process.env.PAYROLL_CHANNEL_ID; +const QUEUE_URL = process.env.CONTRACTOR_BATCH_QUEUE_URL; + +let cachedToken; +async function getSlackToken() { + if (cachedToken) return cachedToken; + const { Parameter } = await ssm.send( + new GetParameterCommand({ + Name: process.env.SLACK_BOT_TOKEN_PARAM, + WithDecryption: true, + }) + ); + cachedToken = Parameter.Value; + return cachedToken; +} + +const formatCurrency = (v) => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(Number(v || 0)); + +function classifyEmail(from, subject, text) { + const fromAddr = (from?.text || from || "").toLowerCase(); + const subj = (subject || "").toLowerCase(); + + if ( + fromAddr.includes("automated@gusto.com") && + subj.includes("payroll confirmation") + ) { + return "employee"; + } + if ( + fromAddr.includes("gustonoreply@gusto.com") && + subj.includes("payment confirmation") + ) { + return "contractor"; + } + + const strippedSubj = subj.replace(/^fwd?:\s*/i, ""); + const body = (text || "").toLowerCase(); + const bodyMentionsGusto = body.includes("gusto.com"); + + if (bodyMentionsGusto && strippedSubj.includes("payroll confirmation")) { + return "employee"; + } + if (bodyMentionsGusto && strippedSubj.includes("payment confirmation")) { + return "contractor"; + } + + return null; +} + +function parseDateFromSubject(subject) { + const m = subject.match(/:\s*(\w+,\s*\w+\s+\d+)\s+(?:payroll|payment)\s+confirmation/i); + if (!m) return null; + const raw = m[1].trim(); + const year = new Date().getFullYear(); + const d = new Date(`${raw}, ${year}`); + if (isNaN(d)) return null; + return { + display: raw, + iso: d.toISOString().slice(0, 10), + }; +} + +function parseEmployeePayroll(subject, text) { + const checkDate = parseDateFromSubject(subject); + if (!checkDate) return null; + + const norm = text.replace(/\n/g, " ").replace(/\s+/g, " "); + + const debitMatch = norm.match( + /we.ll debit \$([\d,]+\.\d{2}) from the bank account ending in (\d+)/i + ); + if (!debitMatch) return null; + + const totalDebit = parseFloat(debitMatch[1].replace(/,/g, "")); + const bankSuffix = debitMatch[2]; + + const payPeriodMatch = norm.match(/payroll for the (.+?)\s+pay period/i); + const netPayMatch = norm.match( + /\$([\d,]+\.\d{2}) will be for your employee net pay/i + ); + const taxesMatch = norm.match(/\$([\d,]+\.\d{2}) will be for taxes/i); + const reimbursementsMatch = norm.match( + /\$([\d,]+\.\d{2}) will be for reimbursements/i + ); + + return { + checkDate: checkDate.display, + dateKey: checkDate.iso, + totalDebit, + bankSuffix, + payPeriod: payPeriodMatch ? payPeriodMatch[1].trim() : null, + netPay: netPayMatch ? parseFloat(netPayMatch[1].replace(/,/g, "")) : null, + taxes: taxesMatch ? parseFloat(taxesMatch[1].replace(/,/g, "")) : null, + reimbursements: reimbursementsMatch + ? parseFloat(reimbursementsMatch[1].replace(/,/g, "")) + : null, + }; +} + +function parseContractorPayment(subject, text) { + const checkDate = parseDateFromSubject(subject); + if (!checkDate) return null; + + const norm = text.replace(/\n/g, " ").replace(/\s+/g, " "); + + const debitMatch = norm.match( + /we.ll debit \$([\d,]+\.\d{2}) from the bank account ending in (\d+)/i + ); + if (!debitMatch) return null; + + const totalDebit = parseFloat(debitMatch[1].replace(/,/g, "")); + const bankSuffix = debitMatch[2]; + + const nameMatch = norm.match(/at that time for (.+?) to be paid on/i); + const contractorName = nameMatch ? nameMatch[1].trim() : "Unknown contractor"; + + return { + checkDate: checkDate.display, + dateKey: checkDate.iso, + totalDebit, + bankSuffix, + contractorName, + }; +} + +function buildCombinedBlocks(dateDisplay, employee, contractors) { + const blocks = [ + { + type: "header", + text: { + type: "plain_text", + text: `Payroll Processed — ${dateDisplay}`, + }, + }, + ]; + + if (employee) { + const topFields = [ + { type: "mrkdwn", text: `*Check Date*\n${employee.checkDate}` }, + ]; + if (employee.payPeriod) { + topFields.unshift({ + type: "mrkdwn", + text: `*Pay Period*\n${employee.payPeriod}`, + }); + } + blocks.push({ type: "section", fields: topFields }); + blocks.push({ type: "divider" }); + + const detailFields = []; + if (employee.netPay != null) { + detailFields.push({ + type: "mrkdwn", + text: `*Employee Net Pay*\n${formatCurrency(employee.netPay)}`, + }); + } + if (employee.taxes != null) { + detailFields.push({ + type: "mrkdwn", + text: `*Taxes*\n${formatCurrency(employee.taxes)}`, + }); + } + if (detailFields.length) blocks.push({ type: "section", fields: detailFields }); + + const extraFields = []; + if (employee.reimbursements != null) { + extraFields.push({ + type: "mrkdwn", + text: `*Reimbursements*\n${formatCurrency(employee.reimbursements)}`, + }); + } + extraFields.push({ + type: "mrkdwn", + text: `*Bank Account*\n...${employee.bankSuffix}`, + }); + blocks.push({ type: "section", fields: extraFields }); + } + + if (contractors.length > 0) { + blocks.push({ type: "divider" }); + blocks.push({ + type: "section", + text: { + type: "mrkdwn", + text: `*Contractor Payments*`, + }, + }); + for (const c of contractors) { + blocks.push({ + type: "section", + fields: [ + { type: "mrkdwn", text: `*Contractor*\n${c.contractorName}` }, + { type: "mrkdwn", text: `*Amount*\n${formatCurrency(c.totalDebit)}` }, + ], + }); + } + } + + const grandTotal = + (employee ? employee.totalDebit : 0) + + contractors.reduce((sum, c) => sum + c.totalDebit, 0); + + blocks.push({ type: "divider" }); + blocks.push({ + type: "section", + text: { + type: "mrkdwn", + text: `:moneybag: *Total Bank Debit: ${formatCurrency(grandTotal)}*`, + }, + }); + blocks.push({ + type: "context", + elements: [ + { type: "mrkdwn", text: "Source: Gusto payroll confirmation emails" }, + ], + }); + + return { blocks, grandTotal }; +} + +async function postSlack(token, blocks, text) { + const res = await fetch("https://slack.com/api/chat.postMessage", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ channel: CHANNEL_ID, blocks, text }), + }); + const data = await res.json(); + if (!data.ok) { + console.error("Slack post failed:", data.error); + throw new Error(`Slack API error: ${data.error}`); + } + return data.ts; +} + +const ttl90Days = () => Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60; + +async function handleS3Event(record) { + const bucket = record.s3.bucket.name; + const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); + + const { Body } = await s3.send( + new GetObjectCommand({ Bucket: bucket, Key: key }) + ); + const rawEmail = await Body.transformToByteArray(); + const parsed = await simpleParser(Buffer.from(rawEmail)); + + const type = classifyEmail(parsed.from, parsed.subject, parsed.text); + if (!type) { + console.log("Unrecognized email, skipping:", parsed.subject); + return; + } + + if (type === "employee") { + const data = parseEmployeePayroll(parsed.subject, parsed.text); + if (!data) { + console.error("Failed to parse employee payroll:", parsed.subject); + return; + } + + const pk = `PAYROLL_EMAIL#employee#${data.dateKey}`; + const existing = await ddb.send( + new GetCommand({ TableName: TABLE_NAME, Key: { pk } }) + ); + if (existing.Item) { + console.log(`Already recorded: employee ${data.dateKey}`); + return; + } + + await ddb.send( + new PutCommand({ + TableName: TABLE_NAME, + Item: { + pk, + type: "employee", + status: "pending", + checkDate: data.checkDate, + dateKey: data.dateKey, + totalDebit: data.totalDebit, + bankSuffix: data.bankSuffix, + payPeriod: data.payPeriod, + netPay: data.netPay, + taxes: data.taxes, + reimbursements: data.reimbursements, + processedAt: new Date().toISOString(), + ttl: ttl90Days(), + }, + }) + ); + + await sqs.send( + new SendMessageCommand({ + QueueUrl: QUEUE_URL, + MessageBody: JSON.stringify({ dateKey: data.dateKey }), + }) + ); + console.log(`Queued employee payroll for ${data.dateKey}`); + } + + if (type === "contractor") { + const data = parseContractorPayment(parsed.subject, parsed.text); + if (!data) { + console.error("Failed to parse contractor payment:", parsed.subject); + return; + } + + const pk = `PAYROLL_EMAIL#contractor#${data.dateKey}#${data.bankSuffix}`; + const existing = await ddb.send( + new GetCommand({ TableName: TABLE_NAME, Key: { pk } }) + ); + if (existing.Item) { + console.log( + `Already recorded: contractor ${data.contractorName} ${data.dateKey}` + ); + return; + } + + await ddb.send( + new PutCommand({ + TableName: TABLE_NAME, + Item: { + pk, + type: "contractor", + status: "pending", + checkDate: data.checkDate, + dateKey: data.dateKey, + totalDebit: data.totalDebit, + bankSuffix: data.bankSuffix, + contractorName: data.contractorName, + processedAt: new Date().toISOString(), + ttl: ttl90Days(), + }, + }) + ); + + await sqs.send( + new SendMessageCommand({ + QueueUrl: QUEUE_URL, + MessageBody: JSON.stringify({ dateKey: data.dateKey }), + }) + ); + console.log( + `Queued contractor: ${data.contractorName} for ${data.dateKey}` + ); + } +} + +async function handleSqsEvent(sqsRecord) { + const { dateKey } = JSON.parse(sqsRecord.body); + + const batchPk = `PAYROLL_BATCH#${dateKey}`; + const batchCheck = await ddb.send( + new GetCommand({ TableName: TABLE_NAME, Key: { pk: batchPk } }) + ); + if (batchCheck.Item) { + console.log(`Batch already posted for ${dateKey}`); + return; + } + + const { Items } = await ddb.send( + new ScanCommand({ + TableName: TABLE_NAME, + FilterExpression: "begins_with(pk, :prefix) AND #s = :pending", + ExpressionAttributeNames: { "#s": "status" }, + ExpressionAttributeValues: { + ":prefix": `PAYROLL_EMAIL#`, + ":pending": "pending", + }, + }) + ); + + const pending = (Items || []).filter((i) => i.dateKey === dateKey); + if (pending.length === 0) { + console.log(`No pending items for ${dateKey}`); + return; + } + + const employee = pending.find((i) => i.type === "employee") || null; + const contractors = pending.filter((i) => i.type === "contractor"); + const dateDisplay = pending[0].checkDate; + + const token = await getSlackToken(); + const { blocks, grandTotal } = buildCombinedBlocks(dateDisplay, employee, contractors); + const slackTs = await postSlack( + token, + blocks, + `Payroll processed for ${dateDisplay}: ${formatCurrency(grandTotal)}` + ); + + await ddb.send( + new PutCommand({ + TableName: TABLE_NAME, + Item: { + pk: batchPk, + dateKey, + checkDate: dateDisplay, + employeeCount: employee ? 1 : 0, + contractorCount: contractors.length, + totalDebit: grandTotal, + slackTs, + processedAt: new Date().toISOString(), + ttl: ttl90Days(), + }, + }) + ); + + const updateBatch = pending.map((item) => ({ + PutRequest: { + Item: { ...item, status: "notified" }, + }, + })); + for (let i = 0; i < updateBatch.length; i += 25) { + await ddb.send( + new BatchWriteCommand({ + RequestItems: { [TABLE_NAME]: updateBatch.slice(i, i + 25) }, + }) + ); + } + + console.log( + `Posted batch: ${employee ? 1 : 0} employee + ${contractors.length} contractor(s) for ${dateKey}` + ); +} + +export const handler = async (event) => { + if (event.Records?.[0]?.eventSource === "aws:s3") { + for (const record of event.Records) { + await handleS3Event(record); + } + } else if (event.Records?.[0]?.eventSource === "aws:sqs") { + for (const record of event.Records) { + await handleSqsEvent(record); + } + } else { + console.log("Unknown event source:", JSON.stringify(event).slice(0, 200)); + } + + return { statusCode: 200 }; +}; diff --git a/template.yaml b/template.yaml index c15adc7..5d57f93 100644 --- a/template.yaml +++ b/template.yaml @@ -82,23 +82,6 @@ Resources: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable - PublicSubnetB: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref Vpc - CidrBlock: 10.20.3.0/24 - AvailabilityZone: !Select [1, !GetAZs ""] - MapPublicIpOnLaunch: true - Tags: - - Key: Name - Value: payments-dashboard-public-b - - PublicSubnetBRouteTableAssociation: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - SubnetId: !Ref PublicSubnetB - RouteTableId: !Ref PublicRouteTable - PrivateRouteTable: Type: AWS::EC2::RouteTable Properties: @@ -126,55 +109,6 @@ Resources: - IpProtocol: "-1" CidrIp: 0.0.0.0/0 - # Aurora Serverless v2 PostgreSQL (Dataddo → payroll data) - AuroraSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupDescription: Aurora PostgreSQL access - VpcId: !Ref Vpc - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 5432 - ToPort: 5432 - SourceSecurityGroupId: !Ref LambdaSecurityGroup - - IpProtocol: tcp - FromPort: 5432 - ToPort: 5432 - CidrIp: 0.0.0.0/0 - - AuroraSubnetGroup: - Type: AWS::RDS::DBSubnetGroup - Properties: - DBSubnetGroupDescription: Public subnets for Aurora PostgreSQL - SubnetIds: - - !Ref PublicSubnet - - !Ref PublicSubnetB - - AuroraCluster: - Type: AWS::RDS::DBCluster - Properties: - Engine: aurora-postgresql - EngineVersion: "16.4" - DatabaseName: payroll - MasterUsername: payroll_admin - ManageMasterUserPassword: true - ServerlessV2ScalingConfiguration: - MinCapacity: 0.5 - MaxCapacity: 2 - VpcSecurityGroupIds: - - !Ref AuroraSecurityGroup - DBSubnetGroupName: !Ref AuroraSubnetGroup - EnableHttpEndpoint: true - StorageEncrypted: true - - AuroraInstance: - Type: AWS::RDS::DBInstance - Properties: - DBClusterIdentifier: !Ref AuroraCluster - DBInstanceClass: db.serverless - Engine: aurora-postgresql - PubliclyAccessible: true - PaymentsCsvBucket: Type: AWS::S3::Bucket Properties: @@ -195,6 +129,98 @@ Resources: AttributeName: ttl Enabled: true + # Payroll email ingestion (replaces Dataddo/Aurora pipeline) + PayrollEmailBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub seahaven-payroll-emails-${AWS::AccountId} + LifecycleConfiguration: + Rules: + - Id: ExpireEmails + Status: Enabled + ExpirationInDays: 30 + + PayrollEmailBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: !Ref PayrollEmailBucket + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: AllowSESPut + Effect: Allow + Principal: + Service: ses.amazonaws.com + Action: s3:PutObject + Resource: !Sub arn:aws:s3:::seahaven-payroll-emails-${AWS::AccountId}/* + Condition: + StringEquals: + AWS:SourceAccount: !Ref AWS::AccountId + + PayrollEmailRule: + Type: AWS::SES::ReceiptRule + DependsOn: PayrollEmailBucketPolicy + Properties: + RuleSetName: INBOUND_MAIL + After: ExistingRuleSetWorkorderEmailRuleEA29F845-okepxcVarTfu + Rule: + Name: store-payroll-emails + Enabled: true + ScanEnabled: true + Recipients: + - payroll@int.seahaven.com + Actions: + - S3Action: + BucketName: !Ref PayrollEmailBucket + ObjectKeyPrefix: inbound/ + + ContractorBatchQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: payments-contractor-batch + DelaySeconds: 600 + MessageRetentionPeriod: 86400 + VisibilityTimeout: 60 + + ProcessPayrollEmailFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: payments-processPayrollEmail + Handler: src/processPayrollEmail.handler + Timeout: 60 + Environment: + Variables: + SLACK_BOT_TOKEN_PARAM: /payments-dashboard/slack-bot-token + PAYROLL_CHANNEL_ID: C0AV5RBMYKU + CONTRACTOR_BATCH_QUEUE_URL: !Ref ContractorBatchQueue + Events: + EmailReceived: + Type: S3 + Properties: + Bucket: !Ref PayrollEmailBucket + Events: s3:ObjectCreated:* + Filter: + S3Key: + Rules: + - Name: prefix + Value: inbound/ + ContractorBatch: + Type: SQS + Properties: + Queue: !GetAtt ContractorBatchQueue.Arn + BatchSize: 1 + Policies: + - S3ReadPolicy: + BucketName: !Sub seahaven-payroll-emails-${AWS::AccountId} + - DynamoDBCrudPolicy: + TableName: !Ref DashboardTable + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/slack-bot-token + - SQSSendMessagePolicy: + QueueName: !GetAtt ContractorBatchQueue.QueueName + - SQSPollerPolicy: + QueueName: !GetAtt ContractorBatchQueue.QueueName + ProcessPaymentCsvFunction: Type: AWS::Serverless::Function Properties: @@ -330,51 +356,6 @@ Resources: - ec2:DeleteNetworkInterface Resource: "*" - NotifyPayrollFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: payments-notifyPayroll - Handler: src/notifyPayroll.handler - Timeout: 60 - VpcConfig: - SubnetIds: - - !Ref PrivateSubnet - SecurityGroupIds: - - !Ref LambdaSecurityGroup - Environment: - Variables: - AURORA_CLUSTER_ARN: !GetAtt AuroraCluster.DBClusterArn - AURORA_SECRET_ARN: !GetAtt AuroraCluster.MasterUserSecret.SecretArn - SLACK_BOT_TOKEN_PARAM: /payments-dashboard/slack-bot-token - Events: - PayrollCheck: - Type: Schedule - Properties: - Schedule: cron(0 18 ? * MON-FRI *) - Description: Check for new payroll data at 2pm ET (18:00 UTC) - Enabled: true - Policies: - - DynamoDBCrudPolicy: - TableName: !Ref DashboardTable - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/slack-bot-token - - Version: "2012-10-17" - Statement: - - Effect: Allow - Action: - - ec2:CreateNetworkInterface - - ec2:DescribeNetworkInterfaces - - ec2:DeleteNetworkInterface - Resource: "*" - - Effect: Allow - Action: - - rds-data:ExecuteStatement - Resource: !GetAtt AuroraCluster.DBClusterArn - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: !GetAtt AuroraCluster.MasterUserSecret.SecretArn - Outputs: SlackEventUrl: Description: URL to set as the Slack app Request URL @@ -385,12 +366,6 @@ Outputs: StaticOutboundIp: Description: Static IP for BoA API whitelist Value: !Ref NatEip - AuroraEndpoint: - Description: Aurora PostgreSQL cluster endpoint - Value: !GetAtt AuroraCluster.Endpoint.Address - AuroraPort: - Description: Aurora PostgreSQL port - Value: !GetAtt AuroraCluster.Endpoint.Port - AuroraSecretArn: - Description: Secrets Manager ARN for Aurora master credentials - Value: !GetAtt AuroraCluster.MasterUserSecret.SecretArn + PayrollEmailBucket: + Description: S3 bucket for inbound payroll emails from SES + Value: !Ref PayrollEmailBucket