Auspicious14 commited on
Commit
cdd93a9
Β·
1 Parent(s): 06b1815

feat(notifications): replace Resend with Gmail API for email delivery

Browse files

Switch 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 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 { Resend } from "resend";
 
 
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  @Injectable()
6
- export class EmailService {
7
  private readonly logger = new Logger(EmailService.name);
8
- private readonly resend: Resend;
9
  private readonly fromAddress: string;
 
 
10
 
11
  constructor(private readonly configService: ConfigService) {
12
- const apiKey = this.configService.get<string>("RESEND_API_KEY");
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- if (!apiKey) {
15
- this.logger.error("RESEND_API_KEY is not set. Emails will not be sent.");
 
 
 
 
 
16
  }
17
 
18
- this.resend = new Resend(apiKey);
19
- this.fromAddress =
20
- this.configService.get<string>("SMTP_FROM") ||
21
- "MaternAlert <onboarding@resend.dev>";
 
 
 
 
 
 
 
 
22
  }
23
 
24
  async sendEmail(
@@ -27,8 +69,16 @@ export class EmailService {
27
  text: string,
28
  html?: string,
29
  ): Promise<boolean> {
 
 
 
 
 
 
 
30
  try {
31
- const { data, error } = await this.resend.emails.send({
 
32
  from: this.fromAddress,
33
  to,
34
  subject,
@@ -36,19 +86,42 @@ export class EmailService {
36
  html: html || text,
37
  });
38
 
39
- if (error) {
40
- this.logger.error(`Failed to send email to ${to}: ${error.message}`);
41
- return false;
42
- }
43
 
44
- this.logger.log(`Email sent successfully: ${data?.id} to ${to}`);
45
  return true;
46
  } catch (error: any) {
47
  this.logger.error(
48
- `Unexpected error sending email to ${to}: ${error.message}`,
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
  }