Auspicious14 commited on
Commit
56bc31d
·
1 Parent(s): 8d937c9

fix(email): enforce IPv4 DNS resolution and add retry logic

Browse files

Force IPv4-first DNS resolution globally and in email service to prevent
IPv6 connection failures with SMTP hosts like Gmail. Add retry logic with
2 attempts and shorter timeouts to improve email delivery reliability.

Files changed (2) hide show
  1. src/main.ts +7 -1
  2. src/notifications/email.service.ts +57 -33
src/main.ts CHANGED
@@ -1,3 +1,9 @@
 
 
 
 
 
 
1
  import { NestFactory } from "@nestjs/core";
2
  import { AppModule } from "./app.module";
3
  import { setupGlobalMiddleware } from "./setup";
@@ -8,7 +14,7 @@ async function bootstrap() {
8
  setupGlobalMiddleware(app);
9
 
10
  const port = process.env.PORT || 3000;
11
- await app.listen(port, '0.0.0.0');
12
 
13
  console.log(`\n🏥 Maternal Health Support Backend`);
14
  console.log(`⚠️ CARE-SUPPORT TOOL - NOT A DIAGNOSTIC SYSTEM`);
 
1
+ // Force IPv4-first DNS globally — must be the very first import to prevent
2
+ // nodemailer (and any other net module) from attempting IPv6 connections on
3
+ // hosts that don't support it (e.g. smtp.gmail.com port 587).
4
+ import * as dns from "dns";
5
+ dns.setDefaultResultOrder("ipv4first");
6
+
7
  import { NestFactory } from "@nestjs/core";
8
  import { AppModule } from "./app.module";
9
  import { setupGlobalMiddleware } from "./setup";
 
14
  setupGlobalMiddleware(app);
15
 
16
  const port = process.env.PORT || 3000;
17
+ await app.listen(port, "0.0.0.0");
18
 
19
  console.log(`\n🏥 Maternal Health Support Backend`);
20
  console.log(`⚠️ CARE-SUPPORT TOOL - NOT A DIAGNOSTIC SYSTEM`);
src/notifications/email.service.ts CHANGED
@@ -1,6 +1,10 @@
1
  import { Injectable, Logger } from "@nestjs/common";
2
  import { ConfigService } from "@nestjs/config";
3
  import * as nodemailer from "nodemailer";
 
 
 
 
4
 
5
  @Injectable()
6
  export class EmailService {
@@ -8,26 +12,35 @@ export class EmailService {
8
  private transporter: nodemailer.Transporter;
9
 
10
  constructor(private readonly configService: ConfigService) {
 
 
 
 
 
 
 
 
11
  this.transporter = nodemailer.createTransport({
12
- host: this.configService.get("SMTP_HOST"),
13
- port: parseInt(this.configService.get("SMTP_PORT") || "587", 10),
14
- secure: this.configService.get("SMTP_SECURE") === "true",
15
- family: 4, // Force IPv4
16
- pool: false, // Turn off pool to debug fresh connections
17
-
18
- maxConnections: 5,
19
- maxMessages: 100,
20
- connectionTimeout: 20000,
21
- greetingTimeout: 20000,
22
- socketTimeout: 30000,
23
  debug: true,
24
  logger: true,
25
  auth: {
26
- user: this.configService.get("SMTP_USER"),
27
- pass: this.configService.get("SMTP_PASS"),
28
  },
29
  tls: {
30
- rejectUnauthorized: false, // Help with some SMTP issues
 
 
31
  },
32
  } as any);
33
  }
@@ -38,25 +51,36 @@ export class EmailService {
38
  text: string,
39
  html?: string,
40
  ): Promise<boolean> {
41
- try {
42
- const from = this.configService.get("SMTP_FROM");
43
-
44
- const info = await this.transporter.sendMail({
45
- from,
46
- to,
47
- subject,
48
- text,
49
- html: html || text,
50
- });
51
-
52
- this.logger.log(`Email sent successfully: ${info.messageId} to ${to}`);
53
- return true;
54
- } catch (error: any) {
55
- this.logger.error(
56
- `Failed to send email to ${to}: ${error.message}`,
57
- error.stack,
58
- );
59
- return false;
 
 
 
 
 
 
 
 
 
60
  }
 
 
61
  }
62
  }
 
1
  import { Injectable, Logger } from "@nestjs/common";
2
  import { ConfigService } from "@nestjs/config";
3
  import * as nodemailer from "nodemailer";
4
+ import * as dns from "dns";
5
+
6
+ // Force all DNS lookups in this process to prefer IPv4
7
+ dns.setDefaultResultOrder("ipv4first");
8
 
9
  @Injectable()
10
  export class EmailService {
 
12
  private transporter: nodemailer.Transporter;
13
 
14
  constructor(private readonly configService: ConfigService) {
15
+ const smtpHost =
16
+ this.configService.get<string>("SMTP_HOST") || "smtp.gmail.com";
17
+ const smtpPort = parseInt(
18
+ this.configService.get<string>("SMTP_PORT") || "587",
19
+ 10,
20
+ );
21
+ const smtpSecure = this.configService.get<string>("SMTP_SECURE") === "true";
22
+
23
  this.transporter = nodemailer.createTransport({
24
+ host: smtpHost,
25
+ port: smtpPort,
26
+ secure: smtpSecure,
27
+ // Explicitly force IPv4 — prevents nodemailer falling back to IPv6 after a
28
+ // timeout on the IPv4 address when the host resolves to both record types.
29
+ family: 4,
30
+ pool: false,
31
+ connectionTimeout: 15000,
32
+ greetingTimeout: 15000,
33
+ socketTimeout: 20000,
 
34
  debug: true,
35
  logger: true,
36
  auth: {
37
+ user: this.configService.get<string>("SMTP_USER"),
38
+ pass: this.configService.get<string>("SMTP_PASS"),
39
  },
40
  tls: {
41
+ // Must match the real hostname for SNI even if we resolve the IP manually
42
+ servername: smtpHost,
43
+ rejectUnauthorized: false,
44
  },
45
  } as any);
46
  }
 
51
  text: string,
52
  html?: string,
53
  ): Promise<boolean> {
54
+ const from = this.configService.get<string>("SMTP_FROM");
55
+ const maxAttempts = 2;
56
+
57
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
58
+ try {
59
+ const info = await this.transporter.sendMail({
60
+ from,
61
+ to,
62
+ subject,
63
+ text,
64
+ html: html || text,
65
+ });
66
+
67
+ this.logger.log(
68
+ `Email sent successfully (attempt ${attempt}): ${info.messageId} to ${to}`,
69
+ );
70
+ return true;
71
+ } catch (error: any) {
72
+ this.logger.error(
73
+ `Failed to send email to ${to} (attempt ${attempt}/${maxAttempts}): ${error.message}`,
74
+ attempt === maxAttempts ? error.stack : undefined,
75
+ );
76
+
77
+ if (attempt < maxAttempts) {
78
+ // Wait 2 s before retrying
79
+ await new Promise((resolve) => setTimeout(resolve, 2000));
80
+ }
81
+ }
82
  }
83
+
84
+ return false;
85
  }
86
  }