Skip to content

Commit 2fb244c

Browse files
committed
[Feat]: add HTTP API support for sending OTP emails with configurable timeout
1 parent 0fec534 commit 2fb244c

File tree

1 file changed

+64
-0
lines changed

1 file changed

+64
-0
lines changed

src/utils/mailer.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import nodemailer from 'nodemailer';
22
import type { TransportOptions as NodemailerTransportOptions } from 'nodemailer';
3+
import { setTimeout as sleep } from 'timers/promises';
34

45
interface SmtpConfig {
56
host: string;
@@ -18,6 +19,13 @@ interface SmtpConfig {
1819
rejectUnauthorized: boolean;
1920
}
2021

22+
interface HttpMailConfig {
23+
apiKey: string;
24+
apiUrl: string;
25+
from: string;
26+
timeoutMs: number;
27+
}
28+
2129
function getConfig(): SmtpConfig {
2230
const host = process.env.SMTP_HOST;
2331
const port = Number(process.env.SMTP_PORT || 587);
@@ -55,6 +63,15 @@ function getConfig(): SmtpConfig {
5563
};
5664
}
5765

66+
function getHttpConfig(): HttpMailConfig | null {
67+
const apiKey = process.env.SMTP_API_KEY;
68+
const from = process.env.SMTP_FROM;
69+
if (!apiKey || !from) return null;
70+
const apiUrl = process.env.SMTP_API_URL || 'https://api.smtp2go.com/v3/email/send';
71+
const timeoutMs = Number(process.env.SMTP_API_TIMEOUT_MS || 10000);
72+
return { apiKey, apiUrl, from, timeoutMs };
73+
}
74+
5875
function maskEmail(email: string) {
5976
const [local, domain] = email.split('@');
6077
if (!domain) return email;
@@ -116,6 +133,11 @@ type MailTransportOptions = NodemailerTransportOptions & {
116133
};
117134

118135
export async function sendOtpEmail(to: string, code: string) {
136+
const httpCfg = getHttpConfig();
137+
if (httpCfg) {
138+
return sendViaHttpApi(httpCfg, to, code);
139+
}
140+
119141
const cfg = getConfig();
120142
const transportOptions: MailTransportOptions = {
121143
host: cfg.host,
@@ -164,3 +186,45 @@ export async function sendOtpEmail(to: string, code: string) {
164186
throw wrapped;
165187
}
166188
}
189+
190+
async function sendViaHttpApi(cfg: HttpMailConfig, to: string, code: string) {
191+
const startedAt = Date.now();
192+
const controller = new AbortController();
193+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
194+
const payload = {
195+
api_key: cfg.apiKey,
196+
to: [to],
197+
sender: cfg.from,
198+
subject: 'Código de verificación',
199+
text_body: `Tu código de verificación es: ${code}`,
200+
};
201+
try {
202+
const res = await fetch(cfg.apiUrl, {
203+
method: 'POST',
204+
headers: { 'Content-Type': 'application/json' },
205+
body: JSON.stringify(payload),
206+
signal: controller.signal,
207+
});
208+
if (!res.ok) {
209+
const body = await res.text();
210+
throw new Error(`API email error ${res.status}: ${body?.slice(0, 200)}`);
211+
}
212+
const data = (await res.json()) as { data?: { message_id?: string; succeeded?: number } };
213+
console.info(
214+
`[mailer] OTP sent via HTTPS API to ${maskEmail(to)} in ${Date.now() - startedAt}ms (id=${data?.data?.message_id ?? 'n/a'})`,
215+
);
216+
return data;
217+
} catch (error) {
218+
if (error instanceof DOMException && error.name === 'AbortError') {
219+
throw new Error('No se pudo enviar el correo (timeout API).');
220+
}
221+
console.error(
222+
`[mailer] HTTPS email send failed to ${maskEmail(to)} after ${Date.now() - startedAt}ms`,
223+
error,
224+
);
225+
throw error;
226+
} finally {
227+
clearTimeout(timer as any);
228+
await sleep(0); // yield event loop
229+
}
230+
}

0 commit comments

Comments
 (0)