Spaces:
Running
Running
Commit Β·
cdd93a9
1
Parent(s): 06b1815
feat(notifications): replace Resend with Gmail API for email delivery
Browse filesSwitch from Resend to Gmail API to ensure reliable email delivery in containerized environments. Cloud platforms often block outbound SMTP ports, while the Gmail API uses HTTPS which is universally allowed. This change requires new environment variables (GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN) and removes the RESEND_API_KEY dependency.
- package-lock.json +227 -1
- package.json +1 -0
- src/notifications/email.service.ts +91 -18
package-lock.json
CHANGED
|
@@ -26,6 +26,7 @@
|
|
| 26 |
"class-validator": "^0.14.3",
|
| 27 |
"compression": "^1.8.1",
|
| 28 |
"expo-server-sdk": "^6.1.0",
|
|
|
|
| 29 |
"helmet": "^8.1.0",
|
| 30 |
"nodemailer": "^8.0.1",
|
| 31 |
"passport": "^0.7.0",
|
|
@@ -2062,6 +2063,15 @@
|
|
| 2062 |
"node": ">=0.4.0"
|
| 2063 |
}
|
| 2064 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2065 |
"node_modules/agentkeepalive": {
|
| 2066 |
"version": "4.6.0",
|
| 2067 |
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
|
@@ -2234,7 +2244,6 @@
|
|
| 2234 |
"version": "1.5.1",
|
| 2235 |
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
| 2236 |
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
| 2237 |
-
"dev": true,
|
| 2238 |
"funding": [
|
| 2239 |
{
|
| 2240 |
"type": "github",
|
|
@@ -2275,6 +2284,15 @@
|
|
| 2275 |
"node": ">= 18"
|
| 2276 |
}
|
| 2277 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2278 |
"node_modules/bl": {
|
| 2279 |
"version": "4.1.0",
|
| 2280 |
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
|
@@ -2965,6 +2983,15 @@
|
|
| 2965 |
"devOptional": true,
|
| 2966 |
"license": "MIT"
|
| 2967 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2968 |
"node_modules/debug": {
|
| 2969 |
"version": "4.4.3",
|
| 2970 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
@@ -3457,6 +3484,12 @@
|
|
| 3457 |
"devOptional": true,
|
| 3458 |
"license": "MIT"
|
| 3459 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3460 |
"node_modules/fast-check": {
|
| 3461 |
"version": "3.23.2",
|
| 3462 |
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
|
@@ -3523,6 +3556,29 @@
|
|
| 3523 |
],
|
| 3524 |
"license": "BSD-3-Clause"
|
| 3525 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3526 |
"node_modules/file-type": {
|
| 3527 |
"version": "21.2.0",
|
| 3528 |
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.2.0.tgz",
|
|
@@ -3665,6 +3721,18 @@
|
|
| 3665 |
"node": ">= 6"
|
| 3666 |
}
|
| 3667 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3668 |
"node_modules/forwarded": {
|
| 3669 |
"version": "0.2.0",
|
| 3670 |
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
|
@@ -3714,6 +3782,34 @@
|
|
| 3714 |
"url": "https://github.com/sponsors/ljharb"
|
| 3715 |
}
|
| 3716 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3717 |
"node_modules/generate-function": {
|
| 3718 |
"version": "2.3.1",
|
| 3719 |
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
|
@@ -3827,6 +3923,61 @@
|
|
| 3827 |
"url": "https://github.com/sponsors/isaacs"
|
| 3828 |
}
|
| 3829 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3830 |
"node_modules/gopd": {
|
| 3831 |
"version": "1.2.0",
|
| 3832 |
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
|
@@ -3949,6 +4100,19 @@
|
|
| 3949 |
"devOptional": true,
|
| 3950 |
"license": "MIT"
|
| 3951 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3952 |
"node_modules/humanize-ms": {
|
| 3953 |
"version": "1.2.1",
|
| 3954 |
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
|
@@ -4165,6 +4329,15 @@
|
|
| 4165 |
"js-yaml": "bin/js-yaml.js"
|
| 4166 |
}
|
| 4167 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4168 |
"node_modules/json-parse-even-better-errors": {
|
| 4169 |
"version": "2.3.1",
|
| 4170 |
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
|
@@ -4709,6 +4882,26 @@
|
|
| 4709 |
"node": "^18 || ^20 || >= 21"
|
| 4710 |
}
|
| 4711 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4712 |
"node_modules/node-emoji": {
|
| 4713 |
"version": "1.11.0",
|
| 4714 |
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
|
|
@@ -4719,6 +4912,24 @@
|
|
| 4719 |
"lodash": "^4.17.21"
|
| 4720 |
}
|
| 4721 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4722 |
"node_modules/node-fetch-native": {
|
| 4723 |
"version": "1.6.7",
|
| 4724 |
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
|
@@ -6558,6 +6769,12 @@
|
|
| 6558 |
"punycode": "^2.1.0"
|
| 6559 |
}
|
| 6560 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6561 |
"node_modules/util-deprecate": {
|
| 6562 |
"version": "1.0.2",
|
| 6563 |
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
|
@@ -6650,6 +6867,15 @@
|
|
| 6650 |
"defaults": "^1.0.3"
|
| 6651 |
}
|
| 6652 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6653 |
"node_modules/webpack": {
|
| 6654 |
"version": "5.104.1",
|
| 6655 |
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
|
|
|
|
| 26 |
"class-validator": "^0.14.3",
|
| 27 |
"compression": "^1.8.1",
|
| 28 |
"expo-server-sdk": "^6.1.0",
|
| 29 |
+
"googleapis": "^171.4.0",
|
| 30 |
"helmet": "^8.1.0",
|
| 31 |
"nodemailer": "^8.0.1",
|
| 32 |
"passport": "^0.7.0",
|
|
|
|
| 2063 |
"node": ">=0.4.0"
|
| 2064 |
}
|
| 2065 |
},
|
| 2066 |
+
"node_modules/agent-base": {
|
| 2067 |
+
"version": "7.1.4",
|
| 2068 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
| 2069 |
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
| 2070 |
+
"license": "MIT",
|
| 2071 |
+
"engines": {
|
| 2072 |
+
"node": ">= 14"
|
| 2073 |
+
}
|
| 2074 |
+
},
|
| 2075 |
"node_modules/agentkeepalive": {
|
| 2076 |
"version": "4.6.0",
|
| 2077 |
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
|
|
|
| 2244 |
"version": "1.5.1",
|
| 2245 |
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
| 2246 |
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
|
|
|
| 2247 |
"funding": [
|
| 2248 |
{
|
| 2249 |
"type": "github",
|
|
|
|
| 2284 |
"node": ">= 18"
|
| 2285 |
}
|
| 2286 |
},
|
| 2287 |
+
"node_modules/bignumber.js": {
|
| 2288 |
+
"version": "9.3.1",
|
| 2289 |
+
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
| 2290 |
+
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
| 2291 |
+
"license": "MIT",
|
| 2292 |
+
"engines": {
|
| 2293 |
+
"node": "*"
|
| 2294 |
+
}
|
| 2295 |
+
},
|
| 2296 |
"node_modules/bl": {
|
| 2297 |
"version": "4.1.0",
|
| 2298 |
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
|
|
|
| 2983 |
"devOptional": true,
|
| 2984 |
"license": "MIT"
|
| 2985 |
},
|
| 2986 |
+
"node_modules/data-uri-to-buffer": {
|
| 2987 |
+
"version": "4.0.1",
|
| 2988 |
+
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
| 2989 |
+
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
| 2990 |
+
"license": "MIT",
|
| 2991 |
+
"engines": {
|
| 2992 |
+
"node": ">= 12"
|
| 2993 |
+
}
|
| 2994 |
+
},
|
| 2995 |
"node_modules/debug": {
|
| 2996 |
"version": "4.4.3",
|
| 2997 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
|
|
| 3484 |
"devOptional": true,
|
| 3485 |
"license": "MIT"
|
| 3486 |
},
|
| 3487 |
+
"node_modules/extend": {
|
| 3488 |
+
"version": "3.0.2",
|
| 3489 |
+
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
| 3490 |
+
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
| 3491 |
+
"license": "MIT"
|
| 3492 |
+
},
|
| 3493 |
"node_modules/fast-check": {
|
| 3494 |
"version": "3.23.2",
|
| 3495 |
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
|
|
|
| 3556 |
],
|
| 3557 |
"license": "BSD-3-Clause"
|
| 3558 |
},
|
| 3559 |
+
"node_modules/fetch-blob": {
|
| 3560 |
+
"version": "3.2.0",
|
| 3561 |
+
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
| 3562 |
+
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
| 3563 |
+
"funding": [
|
| 3564 |
+
{
|
| 3565 |
+
"type": "github",
|
| 3566 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 3567 |
+
},
|
| 3568 |
+
{
|
| 3569 |
+
"type": "paypal",
|
| 3570 |
+
"url": "https://paypal.me/jimmywarting"
|
| 3571 |
+
}
|
| 3572 |
+
],
|
| 3573 |
+
"license": "MIT",
|
| 3574 |
+
"dependencies": {
|
| 3575 |
+
"node-domexception": "^1.0.0",
|
| 3576 |
+
"web-streams-polyfill": "^3.0.3"
|
| 3577 |
+
},
|
| 3578 |
+
"engines": {
|
| 3579 |
+
"node": "^12.20 || >= 14.13"
|
| 3580 |
+
}
|
| 3581 |
+
},
|
| 3582 |
"node_modules/file-type": {
|
| 3583 |
"version": "21.2.0",
|
| 3584 |
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.2.0.tgz",
|
|
|
|
| 3721 |
"node": ">= 6"
|
| 3722 |
}
|
| 3723 |
},
|
| 3724 |
+
"node_modules/formdata-polyfill": {
|
| 3725 |
+
"version": "4.0.10",
|
| 3726 |
+
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
| 3727 |
+
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
| 3728 |
+
"license": "MIT",
|
| 3729 |
+
"dependencies": {
|
| 3730 |
+
"fetch-blob": "^3.1.2"
|
| 3731 |
+
},
|
| 3732 |
+
"engines": {
|
| 3733 |
+
"node": ">=12.20.0"
|
| 3734 |
+
}
|
| 3735 |
+
},
|
| 3736 |
"node_modules/forwarded": {
|
| 3737 |
"version": "0.2.0",
|
| 3738 |
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
|
|
|
| 3782 |
"url": "https://github.com/sponsors/ljharb"
|
| 3783 |
}
|
| 3784 |
},
|
| 3785 |
+
"node_modules/gaxios": {
|
| 3786 |
+
"version": "7.1.4",
|
| 3787 |
+
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz",
|
| 3788 |
+
"integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==",
|
| 3789 |
+
"license": "Apache-2.0",
|
| 3790 |
+
"dependencies": {
|
| 3791 |
+
"extend": "^3.0.2",
|
| 3792 |
+
"https-proxy-agent": "^7.0.1",
|
| 3793 |
+
"node-fetch": "^3.3.2"
|
| 3794 |
+
},
|
| 3795 |
+
"engines": {
|
| 3796 |
+
"node": ">=18"
|
| 3797 |
+
}
|
| 3798 |
+
},
|
| 3799 |
+
"node_modules/gcp-metadata": {
|
| 3800 |
+
"version": "8.1.2",
|
| 3801 |
+
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
| 3802 |
+
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
| 3803 |
+
"license": "Apache-2.0",
|
| 3804 |
+
"dependencies": {
|
| 3805 |
+
"gaxios": "^7.0.0",
|
| 3806 |
+
"google-logging-utils": "^1.0.0",
|
| 3807 |
+
"json-bigint": "^1.0.0"
|
| 3808 |
+
},
|
| 3809 |
+
"engines": {
|
| 3810 |
+
"node": ">=18"
|
| 3811 |
+
}
|
| 3812 |
+
},
|
| 3813 |
"node_modules/generate-function": {
|
| 3814 |
"version": "2.3.1",
|
| 3815 |
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
|
|
|
| 3923 |
"url": "https://github.com/sponsors/isaacs"
|
| 3924 |
}
|
| 3925 |
},
|
| 3926 |
+
"node_modules/google-auth-library": {
|
| 3927 |
+
"version": "10.6.2",
|
| 3928 |
+
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz",
|
| 3929 |
+
"integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==",
|
| 3930 |
+
"license": "Apache-2.0",
|
| 3931 |
+
"dependencies": {
|
| 3932 |
+
"base64-js": "^1.3.0",
|
| 3933 |
+
"ecdsa-sig-formatter": "^1.0.11",
|
| 3934 |
+
"gaxios": "^7.1.4",
|
| 3935 |
+
"gcp-metadata": "8.1.2",
|
| 3936 |
+
"google-logging-utils": "1.1.3",
|
| 3937 |
+
"jws": "^4.0.0"
|
| 3938 |
+
},
|
| 3939 |
+
"engines": {
|
| 3940 |
+
"node": ">=18"
|
| 3941 |
+
}
|
| 3942 |
+
},
|
| 3943 |
+
"node_modules/google-logging-utils": {
|
| 3944 |
+
"version": "1.1.3",
|
| 3945 |
+
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
|
| 3946 |
+
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
|
| 3947 |
+
"license": "Apache-2.0",
|
| 3948 |
+
"engines": {
|
| 3949 |
+
"node": ">=14"
|
| 3950 |
+
}
|
| 3951 |
+
},
|
| 3952 |
+
"node_modules/googleapis": {
|
| 3953 |
+
"version": "171.4.0",
|
| 3954 |
+
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-171.4.0.tgz",
|
| 3955 |
+
"integrity": "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg==",
|
| 3956 |
+
"license": "Apache-2.0",
|
| 3957 |
+
"dependencies": {
|
| 3958 |
+
"google-auth-library": "^10.2.0",
|
| 3959 |
+
"googleapis-common": "^8.0.0"
|
| 3960 |
+
},
|
| 3961 |
+
"engines": {
|
| 3962 |
+
"node": ">=18"
|
| 3963 |
+
}
|
| 3964 |
+
},
|
| 3965 |
+
"node_modules/googleapis-common": {
|
| 3966 |
+
"version": "8.0.1",
|
| 3967 |
+
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz",
|
| 3968 |
+
"integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==",
|
| 3969 |
+
"license": "Apache-2.0",
|
| 3970 |
+
"dependencies": {
|
| 3971 |
+
"extend": "^3.0.2",
|
| 3972 |
+
"gaxios": "^7.0.0-rc.4",
|
| 3973 |
+
"google-auth-library": "^10.1.0",
|
| 3974 |
+
"qs": "^6.7.0",
|
| 3975 |
+
"url-template": "^2.0.8"
|
| 3976 |
+
},
|
| 3977 |
+
"engines": {
|
| 3978 |
+
"node": ">=18.0.0"
|
| 3979 |
+
}
|
| 3980 |
+
},
|
| 3981 |
"node_modules/gopd": {
|
| 3982 |
"version": "1.2.0",
|
| 3983 |
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
|
|
|
| 4100 |
"devOptional": true,
|
| 4101 |
"license": "MIT"
|
| 4102 |
},
|
| 4103 |
+
"node_modules/https-proxy-agent": {
|
| 4104 |
+
"version": "7.0.6",
|
| 4105 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
| 4106 |
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
| 4107 |
+
"license": "MIT",
|
| 4108 |
+
"dependencies": {
|
| 4109 |
+
"agent-base": "^7.1.2",
|
| 4110 |
+
"debug": "4"
|
| 4111 |
+
},
|
| 4112 |
+
"engines": {
|
| 4113 |
+
"node": ">= 14"
|
| 4114 |
+
}
|
| 4115 |
+
},
|
| 4116 |
"node_modules/humanize-ms": {
|
| 4117 |
"version": "1.2.1",
|
| 4118 |
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
|
|
|
| 4329 |
"js-yaml": "bin/js-yaml.js"
|
| 4330 |
}
|
| 4331 |
},
|
| 4332 |
+
"node_modules/json-bigint": {
|
| 4333 |
+
"version": "1.0.0",
|
| 4334 |
+
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
| 4335 |
+
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
| 4336 |
+
"license": "MIT",
|
| 4337 |
+
"dependencies": {
|
| 4338 |
+
"bignumber.js": "^9.0.0"
|
| 4339 |
+
}
|
| 4340 |
+
},
|
| 4341 |
"node_modules/json-parse-even-better-errors": {
|
| 4342 |
"version": "2.3.1",
|
| 4343 |
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
|
|
|
| 4882 |
"node": "^18 || ^20 || >= 21"
|
| 4883 |
}
|
| 4884 |
},
|
| 4885 |
+
"node_modules/node-domexception": {
|
| 4886 |
+
"version": "1.0.0",
|
| 4887 |
+
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
| 4888 |
+
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
| 4889 |
+
"deprecated": "Use your platform's native DOMException instead",
|
| 4890 |
+
"funding": [
|
| 4891 |
+
{
|
| 4892 |
+
"type": "github",
|
| 4893 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 4894 |
+
},
|
| 4895 |
+
{
|
| 4896 |
+
"type": "github",
|
| 4897 |
+
"url": "https://paypal.me/jimmywarting"
|
| 4898 |
+
}
|
| 4899 |
+
],
|
| 4900 |
+
"license": "MIT",
|
| 4901 |
+
"engines": {
|
| 4902 |
+
"node": ">=10.5.0"
|
| 4903 |
+
}
|
| 4904 |
+
},
|
| 4905 |
"node_modules/node-emoji": {
|
| 4906 |
"version": "1.11.0",
|
| 4907 |
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
|
|
|
|
| 4912 |
"lodash": "^4.17.21"
|
| 4913 |
}
|
| 4914 |
},
|
| 4915 |
+
"node_modules/node-fetch": {
|
| 4916 |
+
"version": "3.3.2",
|
| 4917 |
+
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
| 4918 |
+
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
| 4919 |
+
"license": "MIT",
|
| 4920 |
+
"dependencies": {
|
| 4921 |
+
"data-uri-to-buffer": "^4.0.0",
|
| 4922 |
+
"fetch-blob": "^3.1.4",
|
| 4923 |
+
"formdata-polyfill": "^4.0.10"
|
| 4924 |
+
},
|
| 4925 |
+
"engines": {
|
| 4926 |
+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
| 4927 |
+
},
|
| 4928 |
+
"funding": {
|
| 4929 |
+
"type": "opencollective",
|
| 4930 |
+
"url": "https://opencollective.com/node-fetch"
|
| 4931 |
+
}
|
| 4932 |
+
},
|
| 4933 |
"node_modules/node-fetch-native": {
|
| 4934 |
"version": "1.6.7",
|
| 4935 |
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
|
|
|
| 6769 |
"punycode": "^2.1.0"
|
| 6770 |
}
|
| 6771 |
},
|
| 6772 |
+
"node_modules/url-template": {
|
| 6773 |
+
"version": "2.0.8",
|
| 6774 |
+
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
| 6775 |
+
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
| 6776 |
+
"license": "BSD"
|
| 6777 |
+
},
|
| 6778 |
"node_modules/util-deprecate": {
|
| 6779 |
"version": "1.0.2",
|
| 6780 |
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
|
|
|
| 6867 |
"defaults": "^1.0.3"
|
| 6868 |
}
|
| 6869 |
},
|
| 6870 |
+
"node_modules/web-streams-polyfill": {
|
| 6871 |
+
"version": "3.3.3",
|
| 6872 |
+
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
| 6873 |
+
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
| 6874 |
+
"license": "MIT",
|
| 6875 |
+
"engines": {
|
| 6876 |
+
"node": ">= 8"
|
| 6877 |
+
}
|
| 6878 |
+
},
|
| 6879 |
"node_modules/webpack": {
|
| 6880 |
"version": "5.104.1",
|
| 6881 |
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
|
package.json
CHANGED
|
@@ -65,6 +65,7 @@
|
|
| 65 |
"class-validator": "^0.14.3",
|
| 66 |
"compression": "^1.8.1",
|
| 67 |
"expo-server-sdk": "^6.1.0",
|
|
|
|
| 68 |
"helmet": "^8.1.0",
|
| 69 |
"nodemailer": "^8.0.1",
|
| 70 |
"passport": "^0.7.0",
|
|
|
|
| 65 |
"class-validator": "^0.14.3",
|
| 66 |
"compression": "^1.8.1",
|
| 67 |
"expo-server-sdk": "^6.1.0",
|
| 68 |
+
"googleapis": "^171.4.0",
|
| 69 |
"helmet": "^8.1.0",
|
| 70 |
"nodemailer": "^8.0.1",
|
| 71 |
"passport": "^0.7.0",
|
src/notifications/email.service.ts
CHANGED
|
@@ -1,24 +1,66 @@
|
|
| 1 |
-
import { Injectable, Logger } from "@nestjs/common";
|
| 2 |
import { ConfigService } from "@nestjs/config";
|
| 3 |
-
import {
|
|
|
|
|
|
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
@Injectable()
|
| 6 |
-
export class EmailService {
|
| 7 |
private readonly logger = new Logger(EmailService.name);
|
| 8 |
-
private
|
| 9 |
private readonly fromAddress: string;
|
|
|
|
|
|
|
| 10 |
|
| 11 |
constructor(private readonly configService: ConfigService) {
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
if (!
|
| 15 |
-
this.logger.error(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
async sendEmail(
|
|
@@ -27,8 +69,16 @@ export class EmailService {
|
|
| 27 |
text: string,
|
| 28 |
html?: string,
|
| 29 |
): Promise<boolean> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
try {
|
| 31 |
-
|
|
|
|
| 32 |
from: this.fromAddress,
|
| 33 |
to,
|
| 34 |
subject,
|
|
@@ -36,19 +86,42 @@ export class EmailService {
|
|
| 36 |
html: html || text,
|
| 37 |
});
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
}
|
| 43 |
|
| 44 |
-
this.logger.log(`Email sent
|
| 45 |
return true;
|
| 46 |
} catch (error: any) {
|
| 47 |
this.logger.error(
|
| 48 |
-
`
|
| 49 |
error.stack,
|
| 50 |
);
|
| 51 |
return false;
|
| 52 |
}
|
| 53 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
|
|
|
| 1 |
+
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
| 2 |
import { ConfigService } from "@nestjs/config";
|
| 3 |
+
import { google, gmail_v1 } from "googleapis";
|
| 4 |
+
import * as nodemailer from "nodemailer";
|
| 5 |
+
import Mail from "nodemailer/lib/mailer";
|
| 6 |
|
| 7 |
+
/**
|
| 8 |
+
* EmailService β sends via Gmail REST API (HTTPS port 443).
|
| 9 |
+
*
|
| 10 |
+
* Why not SMTP?
|
| 11 |
+
* Cloud/container hosts (Railway, Render, Fly.io, etc.) block outbound
|
| 12 |
+
* SMTP ports 587 and 465. The Gmail API uses HTTPS which is never blocked.
|
| 13 |
+
*
|
| 14 |
+
* Required env vars:
|
| 15 |
+
* GMAIL_CLIENT_ID β OAuth2 client ID from Google Cloud Console
|
| 16 |
+
* GMAIL_CLIENT_SECRET β OAuth2 client secret
|
| 17 |
+
* GMAIL_REFRESH_TOKEN β Long-lived refresh token (see setup guide below)
|
| 18 |
+
* GMAIL_USER β The Gmail address to send from (e.g. maternalert@gmail.com)
|
| 19 |
+
* SMTP_FROM β Display name + address (e.g. MaternAlert <maternalert@gmail.com>)
|
| 20 |
+
*/
|
| 21 |
@Injectable()
|
| 22 |
+
export class EmailService implements OnModuleInit {
|
| 23 |
private readonly logger = new Logger(EmailService.name);
|
| 24 |
+
private gmail!: gmail_v1.Gmail;
|
| 25 |
private readonly fromAddress: string;
|
| 26 |
+
private readonly gmailUser: string;
|
| 27 |
+
private ready = false;
|
| 28 |
|
| 29 |
constructor(private readonly configService: ConfigService) {
|
| 30 |
+
this.fromAddress =
|
| 31 |
+
this.configService.get<string>("SMTP_FROM") ||
|
| 32 |
+
"MaternAlert <maternalert@gmail.com>";
|
| 33 |
+
|
| 34 |
+
this.gmailUser =
|
| 35 |
+
this.configService.get<string>("GMAIL_USER") || "maternalert@gmail.com";
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
onModuleInit() {
|
| 39 |
+
const clientId = this.configService.get<string>("GMAIL_CLIENT_ID");
|
| 40 |
+
const clientSecret = this.configService.get<string>("GMAIL_CLIENT_SECRET");
|
| 41 |
+
const refreshToken = this.configService.get<string>("GMAIL_REFRESH_TOKEN");
|
| 42 |
|
| 43 |
+
if (!clientId || !clientSecret || !refreshToken) {
|
| 44 |
+
this.logger.error(
|
| 45 |
+
"Gmail API credentials missing. " +
|
| 46 |
+
"Set GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET and GMAIL_REFRESH_TOKEN in .env. " +
|
| 47 |
+
"See: https://developers.google.com/gmail/api/quickstart",
|
| 48 |
+
);
|
| 49 |
+
return;
|
| 50 |
}
|
| 51 |
|
| 52 |
+
const oauth2Client = new google.auth.OAuth2(
|
| 53 |
+
clientId,
|
| 54 |
+
clientSecret,
|
| 55 |
+
// Redirect URI used only during the one-time token exchange
|
| 56 |
+
"https://developers.google.com/oauthplayground",
|
| 57 |
+
);
|
| 58 |
+
|
| 59 |
+
oauth2Client.setCredentials({ refresh_token: refreshToken });
|
| 60 |
+
|
| 61 |
+
this.gmail = google.gmail({ version: "v1", auth: oauth2Client });
|
| 62 |
+
this.ready = true;
|
| 63 |
+
this.logger.log("Gmail API client initialised β
");
|
| 64 |
}
|
| 65 |
|
| 66 |
async sendEmail(
|
|
|
|
| 69 |
text: string,
|
| 70 |
html?: string,
|
| 71 |
): Promise<boolean> {
|
| 72 |
+
if (!this.ready) {
|
| 73 |
+
this.logger.error(
|
| 74 |
+
"Gmail API not initialised β check env vars. Email NOT sent.",
|
| 75 |
+
);
|
| 76 |
+
return false;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
try {
|
| 80 |
+
// Build an RFC 2822 raw message using nodemailer (encoding only, no SMTP)
|
| 81 |
+
const rawMessage = await this.buildRawMessage({
|
| 82 |
from: this.fromAddress,
|
| 83 |
to,
|
| 84 |
subject,
|
|
|
|
| 86 |
html: html || text,
|
| 87 |
});
|
| 88 |
|
| 89 |
+
const res = await this.gmail.users.messages.send({
|
| 90 |
+
userId: "me",
|
| 91 |
+
requestBody: { raw: rawMessage },
|
| 92 |
+
});
|
| 93 |
|
| 94 |
+
this.logger.log(`Email sent via Gmail API β
id=${res.data.id} to=${to}`);
|
| 95 |
return true;
|
| 96 |
} catch (error: any) {
|
| 97 |
this.logger.error(
|
| 98 |
+
`Failed to send email to ${to}: ${error.message}`,
|
| 99 |
error.stack,
|
| 100 |
);
|
| 101 |
return false;
|
| 102 |
}
|
| 103 |
}
|
| 104 |
+
|
| 105 |
+
/**
|
| 106 |
+
* Uses nodemailer purely as an RFC 2822 encoder β no SMTP connection is made.
|
| 107 |
+
*/
|
| 108 |
+
private buildRawMessage(mailOptions: Mail.Options): Promise<string> {
|
| 109 |
+
return new Promise((resolve, reject) => {
|
| 110 |
+
// Null transport: builds the message but never connects to any server
|
| 111 |
+
const transporter = nodemailer.createTransport({ jsonTransport: true });
|
| 112 |
+
|
| 113 |
+
transporter.sendMail(mailOptions, (err, info) => {
|
| 114 |
+
if (err) return reject(err);
|
| 115 |
+
|
| 116 |
+
// jsonTransport gives us the raw message envelope β encode to base64url
|
| 117 |
+
const raw = Buffer.from(info.message)
|
| 118 |
+
.toString("base64")
|
| 119 |
+
.replace(/\+/g, "-")
|
| 120 |
+
.replace(/\//g, "_")
|
| 121 |
+
.replace(/=+$/, "");
|
| 122 |
+
|
| 123 |
+
resolve(raw);
|
| 124 |
+
});
|
| 125 |
+
});
|
| 126 |
+
}
|
| 127 |
}
|